Загадочное «some»: почему о нём спрашивают тысячи разработчиков?
Если вы когда-нибудь писали на SwiftUI, то наверняка видели конструкцию some View. Выглядит она просто, но новички часто теряются: «Что это за «some»? Почему нельзя написать просто View? А если я уберу some — сломается?». Вопрос «What is the "some" keyword in Swift(UI)?» собрал тысячи просмотров на Stack Overflow — и это не случайно. За одним коротким словом скрывается целая философия современного Swift.
В этой статье мы разберёмся, что такое ключевое слово some, зачем оно появилось в языке и почему без него невозможно представить SwiftUI. Вы узнаете, как оно связано с дженериками, протоколами и «непрозрачными типами» (opaque types). А главное — поймёте, как писать более чистый и производительный код.
Проблема, которую решает «some»
Представьте, что вы пишете функцию, которая возвращает какой-то объект, соответствующий протоколу View. В SwiftUI это происходит постоянно:
func makeGreeting() -> View {
return Text("Hello!")
}
Компилятор Swift скажет: «Протокол 'View' может использоваться только как общее ограничение (generic constraint), потому что у него есть Self или associated type requirements». Звучит страшно, но суть проста: протокол View в SwiftUI требует, чтобы тип был известен на этапе компиляции. Если мы возвращаем View — это абстракция, а компилятору нужна конкретика.
Раньше разработчики выкручивались через дженерики:
func makeGreeting<T: View>() -> T {
return Text("Hello!") as! T
}
Это работало, но выглядело как костыль: вы обещаете компилятору, что вернёте T, а на самом деле «притворяетесь». Плюс вызывающий код должен был явно указывать тип. Некрасиво и небезопасно.
«Some» — спаситель от шаблонного кода
Ключевое слово some в Swift появилось в версии 5.1 как раз для таких случаев. Оно говорит компилятору: «Я верну конкретный тип, который соответствует протоколу, но я не хочу называть его имя. Ты сам определи его на этапе компиляции». Это называется непрозрачный тип (opaque type).
Вернёмся к нашему примеру:
func makeGreeting() -> some View {
return Text("Hello!")
}
Теперь код компилируется! Компилятор знает, что возвращается именно Text, но снаружи мы видим только some View. Это даёт две вещи:
- Безопасность типов — компилятор проверяет, что возвращаемый тип всегда один и тот же.
- Инкапсуляция — мы скрываем детали реализации от вызывающего кода.
Сравнение: «some» vs дженерики vs протоколы
Чтобы лучше понять магию some, давайте сравним три подхода на конкретном примере. Допустим, мы хотим написать функцию, которая возвращает фигуру (круг или квадрат).
1. Возврат протокола (не скомпилируется)
protocol Shape {
func draw()
}
struct Circle: Shape {
func draw() { print("Рисуем круг") }
}
struct Square: Shape {
func draw() { print("Рисуем квадрат") }
}
// Ошибка: протокол 'Shape' не может быть использован как тип возврата
func makeShape() -> Shape {
return Circle()
}
Как мы уже знаем — не работает, если у протокола есть associated types или Self requirements.
2. Использование дженериков
func makeShape<T: Shape>() -> T {
// Нужно как-то создать T, но мы не знаем конкретный тип
fatalError("Невозможно создать generic")
}
Проблема: вызывающий код должен знать, какой тип ожидать. Функция не может сама решить, что вернуть.
3. Использование «some» (работает идеально)
func makeShape() -> some Shape {
return Circle()
}
let shape = makeShape() // тип shape — some Shape, но компилятор знает, что это Circle
shape.draw() // Рисуем круг
Ключевой момент: some Shape гарантирует, что функция всегда возвращает один и тот же конкретный тип. Если вы попытаетесь вернуть Circle в одной ветке и Square в другой — компилятор выдаст ошибку. Это фича, а не баг: она предотвращает логические ошибки.
Практика в SwiftUI: почему «some View» везде?
Теперь понятно, почему каждое тело View в SwiftUI помечено как some View:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello")
Image(systemName: "star")
}
}
}
Свойство body возвращает какой-то конкретный тип, соответствующий протоколу View. В данном случае это VStack<TupleView<(Text, Image)>>. Представьте, если бы нам пришлось писать этот тип вручную — это был бы кошмар! some View избавляет нас от этой головной боли.
Более того, если вы используете условную логику в body, компилятор всё равно требует, чтобы возвращался один тип. Поэтому приходится оборачивать ветки в AnyView или использовать Group:
var body: some View {
if condition {
Text("True")
} else {
Image(systemName: "false")
}
}
// Ошибка! Разные типы: Text и Image
Решение — использовать Group или явно привести к AnyView:
var body: some View {
Group {
if condition {
Text("True")
} else {
Image(systemName: "false")
}
}
}
Или, если очень хочется:
var body: some View {
if condition {
return AnyView(Text("True"))
} else {
return AnyView(Image(systemName: "false"))
}
}
Но AnyView — это уже динамическая диспетчеризация, которая чуть медленнее. Поэтому Group предпочтительнее.
«Some» не только для View
Ключевое слово some работает с любыми протоколами, у которых есть associated types или Self requirements. Например, с коллекциями:
func makeCollection() -> some Collection {
return [1, 2, 3]
}
let collection = makeCollection()
print(collection.count) // 3
Или с комбинаторами из Combine:
func makePublisher() -> some Publisher<Int, Never> {
return Just(42)
}