JavaRush /Курси /Go SELF /Варіадичні функції в Go: ...T і scores...

Варіадичні функції в Go: ...T і scores...

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

1. Вступ

Іноді ми не хочемо заздалегідь фіксувати «рівно два числа» або «рівно три рядки». Нам хочеться написати функцію «порахуй суму всіх чисел, які тобі передали» — і щоб вона однаково добре працювала для sum(10, 20), sum(1, 2, 3, 4, 5) і навіть sum() (так, іноді й так буває).

Варіадичні функції в Go — це як «гумова кишеня» в рюкзаку: ви можете покласти туди один предмет, три або десять — кишеня не образиться. Головне, щоб усі предмети були одного типу, бо Go любить порядок і не терпить «салату» з типів без явного зазначення.

До того ж це не якась екзотика: ви користувалися варіадичними функціями ще до того, як дізналися саме слово «variadic». Наприклад, форматований вивід fmt.Printf приймає формат і змінну кількість аргументів після нього — це прямо видно в сигнатурі func Printf(format string, a ...interface{}) (n int, err error).

Синтаксис ...T і правила оголошення

Варіадичний параметр виглядає так: name ...T, де T — тип елементів. Три крапки ... читаються приблизно як «і ще скільки завгодно значень цього типу».

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

Порівняймо звичайну функцію і варіадичну функцію в маленькій табличці:

Що хочемо Як виглядає сигнатура
Рівно два числа
func add(a, b int) int
«Скільки завгодно чисел»
func sum(nums ...int) int
Спочатку обов’язковий параметр, потім «хвіст»
func log(prefix string, values ...int)

Почнімо з найкласичнішої функції суми. Це буде перша цеглинка в нашому маленькому навчальному застосунку — «калькулятор балів» (бали за завдання + бонуси). Нічого серйозного, зате на цьому прикладі зручно показати варіадичність без зайвої магії.

package main

import "fmt"

func sum(nums ...int) int {
	total := 0
	for _, v := range nums {
		total += v
	}
	return total
}

func main() {
	fmt.Println(sum(1, 2, 3)) // 6
}

Зверніть увагу: всередині sum ми спокійно робимо for _, v := range nums. Це нормальний шаблон: варіадичний параметр зручно обходити циклом range.

2. Виклик варіадичних функцій

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

Важливо заздалегідь продумати й зафіксувати для себе, що означає виклик без аргументів. Наприклад, «сума порожнього набору» — це 0. Це логічно й зручно.

Давайте розширимо приклад і подивимося на різні виклики:

package main

import "fmt"

func sum(nums ...int) int {
	total := 0
	for _, v := range nums {
		total += v
	}
	return total
}

func main() {
	fmt.Println(sum())           // 0
	fmt.Println(sum(10))         // 10
	fmt.Println(sum(10, 20, 30)) // 60
}

Тут немає жодного особливого перевантаження: це один і той самий виклик однієї функції.

Тепер зробімо наступний крок у нашому міні-застосунку «калькулятор балів». Нехай у нас є «база» (наприклад, основний бал), а бонуси — необов’язкові.

package main

import "fmt"

func totalScore(base int, bonuses ...int) int {
	return base + sum(bonuses...)
}

func sum(nums ...int) int {
	total := 0
	for _, v := range nums {
		total += v
	}
	return total
}

func main() {
	fmt.Println(totalScore(100))           // 100
	fmt.Println(totalScore(100, 5))        // 105
	fmt.Println(totalScore(100, 5, 10))    // 115
	fmt.Println(totalScore(100, 5, 10, 2)) // 117
}

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

4. Варіадичні параметри всередині функцій

len, range і порожній виклик

Усередині функції варіадичний параметр поводиться як «набір значень»: у нього можна дізнатися довжину через len, можна пройтися range, можна перевірити, чи взагалі щось передали. Це дуже зручно, бо майже завжди варіадичні функції потребують явної поведінки для крайніх випадків: 0 значень, 1 значення і так далі.

Давайте напишемо функцію average, яка обчислює середнє. Середнє для порожнього набору чисел — штука філософська (можна сперечатися до ранку), тому ми домовимося так: якщо чисел немає, це помилка. І це чудовий момент, щоб закріпити контракт (T, error) у зв’язці з варіадичним параметром.

package main

import (
	"errors"
	"fmt"
)

func average(nums ...int) (float64, error) {
	if len(nums) == 0 {
		return 0, errors.New("no numbers to average")
	}
	total := sum(nums...)
	return float64(total) / float64(len(nums)), nil
}

func sum(nums ...int) int {
	total := 0
	for _, v := range nums {
		total += v
	}
	return total
}

func main() {
	avg, err := average(10, 20, 30)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(avg) // 20
}

Зверніть увагу на стиль: одразу після виклику ми перевіряємо err, а вже потім використовуємо результат.

Ще один момент, який корисно відчути на практиці: варіадичний параметр можна обробляти як список, але він не зобов’язаний бути «справжнім» списком. На рівні новачка достатньо розуміти так: «функція отримала N чисел, і я проходжу по них циклом».

Обов’язкові параметри і «хвіст»

Дуже частий сценарій: у функції є кілька обов’язкових параметрів, а потім — опціональна частина. Варіадичний параметр ідеально підходить для такого «хвоста».

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

Зробімо функцію printReport. Вона друкуватиме ім’я, кількість значень і середнє. А щоб це було схоже на «шматочок реального застосунку», нехай вона повертає error, якщо балів немає — інакше звіт беззмістовний.

package main

import (
	"errors"
	"fmt"
)

func printReport(name string, scores ...int) error {
	if len(scores) == 0 {
		return errors.New("no scores for report")
	}
	avg, _ := average(scores...) // тут безпечно, ми вже перевірили len(scores) > 0
	fmt.Printf("%s: count=%d avg=%.2f\n", name, len(scores), avg)
	return nil
}

func average(nums ...int) (float64, error) {
	if len(nums) == 0 {
		return 0, errors.New("no numbers to average")
	}
	return float64(sum(nums...)) / float64(len(nums)), nil
}

func sum(nums ...int) int {
	total := 0
	for _, v := range nums {
		total += v
	}
	return total
}

func main() {
	_ = printReport("Ann", 10, 20, 30) // Ann: count=3 avg=20.00
}

Тут одразу кілька корисних спостережень.

По-перше, scores ...int має стояти останнім, інакше компілятор не зрозуміє межі «хвоста».

По-друге, fmt.Printf ми використовуємо як функцію форматованого виведення. І це не випадкова магія: у цієї функції якраз варіадичні аргументи після рядка формату.

По-третє, ми показали одну акуратну техніку: якщо ми вже перевірили умову, то інколи можна далі викликати функцію й свідомо ігнорувати помилку — але лише коли ви залізно впевнені, що помилки не буде. Це не «лайфхак», а просто дисципліна: ви маєте вміти пояснити, чому це безпечно.

5. Передавання варіадичних аргументів: scores...

Дуже часто варіадичні функції будують шарами. Одна функція приймає «хвіст», робить частину роботи й передає цей хвіст далі в іншу функцію. Тоді під час виклику з’являється синтаксис something....

Це виглядає так: якщо у вас є варіадичний параметр scores ...int, то всередині функції scores — це вже «зібраний набір», і щоб передати його як набір окремих аргументів в іншу варіадичну функцію, ви пишете scores....

Давайте підкреслимо це простим прикладом на «сумі з підписом».

package main

import "fmt"

func sum(nums ...int) int {
	total := 0
	for _, v := range nums {
		total += v
	}
	return total
}

func printSum(label string, nums ...int) {
	fmt.Println(label, sum(nums...))
}

func main() {
	printSum("points:", 5, 10, 3) // points: 18
}

Тут printSum не зобов’язаний перераховувати суму вручну. Він просто делегує роботу sum, передаючи весь «хвіст» далі.

Навіщо взагалі потрібні ці ... під час передавання? Тому що без них ви передаєте «одну штуку» (внутрішнє подання набору), а з ... — «розгорни набір в окремі аргументи». На цьому етапі достатньо запам’ятати практику, а не теорію: якщо функція приймає ...int, то щоб передати далі в ...int, найімовірніше, напишете nums....

Тепер зберімо повну мініверсію нашого застосунку. Ми зробимо main, який читає ім’я, три обов’язкові бали й кількість бонусів (0, 1 або 2). Так, це трохи штучно, бо без більш просунутих тем важко показати читання довільної кількості чисел, але як навчальний приклад — чудово: ми побачимо виклик варіадичної функції з 0, 1 або 2 аргументами.

package main

import (
	"fmt"
)

func sum(nums ...int) int {
	total := 0
	for _, v := range nums {
		total += v
	}
	return total
}

func totalScore(base int, bonuses ...int) int {
	return base + sum(bonuses...)
}

func main() {
	var name string
	var s1, s2, s3 int
	var bonusCount int

	fmt.Scan(&name, &s1, &s2, &s3, &bonusCount)

	base := sum(s1, s2, s3)

	if bonusCount == 0 {
		fmt.Println(name, totalScore(base)) // без бонусів
		return
	}
	if bonusCount == 1 {
		var b1 int
		fmt.Scan(&b1)
		fmt.Println(name, totalScore(base, b1))
		return
	}

	// bonusCount >= 2 (для простоти вважаємо, що максимум 2)
	var b1, b2 int
	fmt.Scan(&b1, &b2)
	fmt.Println(name, totalScore(base, b1, b2))
}

Тут ми акуратно тримаємо стиль «ранній вихід». Зверніть увагу, як зручно читається totalScore(base, b1, b2) — ніби ми викликали звичайну функцію, просто аргументів трохи більше.

6. Типові помилки під час роботи з варіадичними функціями

Помилка №1: спроба поставити варіадичний параметр не останнім.
Новачок інколи пише щось на кшталт func f(nums ...int, label string) і дивується, чому компілятор свариться. Логіка Go тут дуже прагматична: якщо ...int не останній, компілятор не зможе зрозуміти, де закінчилися числа і почався рядок. Виправляється це просто: варіадичний параметр — завжди хвіст, а все обов’язкове ставимо перед ним.

Помилка №2: не визначити поведінку для «порожнього виклику».
Якщо у вас sum(nums ...int) — чудово, порожній виклик sum() природно повертає 0. Але якщо у вас «середнє» або «максимум», порожній виклик стає проблемою: чому дорівнює максимум порожнього набору? Часто правильна відповідь — «це помилка», і тоді сигнатура має стати (T, error), а всередині з’явиться перевірка if len(nums) == 0 { ... }. Найгірше, коли поведінка не визначена і в одному місці ви вважаєте, що «0 — ок», а в іншому — що «0 — це вже готова відповідь».

Помилка №3: забути ... під час передавання аргументів далі.
Ситуація типова: ви написали func printSum(nums ...int), усередині хочете викликати sum(nums) — і ловите помилку компіляції. У таких місцях потрібно пам’ятати просте правило: щоб «розгорнути» варіадичний набір і передати його як окремі аргументи в іншу варіадичну функцію, пишеться nums.... Без трьох крапок Go сприймає це як «передаю один аргумент», а не «передаю багато».

Помилка №4: ігнорувати err там, де варіадична функція може повернути помилку.
Щойно ви зробили функцію на кшталт average(nums ...int) (float64, error), вам доведеться дисципліновано перевіряти помилку. Дуже спокусливо написати avg, _ := average(...) «щоб не заважало», особливо коли здається, що вхід завжди коректний. Але якщо вхід одного разу виявиться порожнім або ви пізніше повторно використаєте функцію в іншому місці, програма почне поводитися дивно: ви будете друкувати 0.00 і думати, що «математика зламалася». На практиці зламалася не математика, а обробка помилок.

Помилка №5: використовувати варіадичність там, де контракт насправді фіксований.
Іноді пишуть func add(nums ...int) int, а потім завжди викликають add(a, b) і ніколи не передають більше або менше. Формально це працює, але читабельність страждає: за назвою і за змістом здається, що функція може приймати багато чисел, хоча бізнес-логіка очікує рівно два. Краще обирати варіадичний параметр лише там, де «змінна кількість» справді є частиною контракту, інакше ви ускладнюєте читання коду майбутньому собі (а майбутній ви, як відомо, людина мстива й без кави).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ