17 octombrie 2026 · 15 min. de citit
some vs any în Swift: diferența și când să folosești fiecare
Dacă dezvolți aplicații iOS de ceva vreme, probabil îți amintești perioada în care puteai folosi numele unui protocol drept tipul unei variabile, al unui parametru sau al unei valori returnate, fără alte cuvinte-cheie.
Swift 5.1 a introdus some, iar Swift 5.6 a adăugat any. În loc de simplul Protocol, întâlnim tot mai des some Protocol și any Protocol. La prima vedere, ambele ascund o implementare în spatele unui protocol. De ce avem nevoie de două mecanisme? De ce some nu poate returna două tipuri concrete diferite din aceeași funcție, dar any poate? Și este some întotdeauna mai rapid?
Să pornim de la un checkout obișnuit, apoi să vedem cum se aplică aceste idei într-un SDK și ce se întâmplă în interiorul Swift.
Să începem cu un protocol obișnuit
Imaginăm un ecran de checkout în care utilizatorul poate plăti cu cardul sau prin cod QR.
import Foundation
protocol PaymentMethod {
func pay(amount: Decimal)
}
struct CardPayment: PaymentMethod {
func pay(amount: Decimal) {
print("Card payment: \(amount)")
}
}
struct QRPayment: PaymentMethod {
func pay(amount: Decimal) {
print("QR payment: \(amount)")
}
}
PaymentMethod definește contractul, iar CardPayment și QRPayment oferă implementări diferite. Acum scriem o funcție care efectuează plata:
func checkout<T: PaymentMethod>(
using method: T,
amount: Decimal
) {
method.pay(amount: amount)
}
Este o funcție generică. În loc să specificăm un tip concret, folosim parametrul de tip T, constrâns să respecte PaymentMethod.
checkout(using: CardPayment(), amount: 100)
checkout(using: QRPayment(), amount: 200)
La primul apel, Swift deduce T ca CardPayment; la al doilea, ca QRPayment. Important este că o funcție generică nu transformă automat argumentul într-un existential. Ea lucrează cu un tip concret T care îndeplinește o constrângere. Identitatea tipului se păstrează chiar dacă funcția a fost scrisă fără să știm ce tip va fi folosit ulterior.
Dar dacă vrem să lucrăm cu implementări diferite prin aceeași interfață?
any: când tipul concret se poate schimba
Să presupunem că utilizatorul poate alege metoda de plată. Vrem să păstrăm opțiunile într-un singur array:
let paymentMethods: [any PaymentMethod] = [
CardPayment(),
QRPayment()
]
Un array Swift are un singur tip pentru elementele sale, însă CardPayment și QRPayment sunt structuri diferite. any PaymentMethod rezolvă problema.
any este un cuvânt-cheie Swift care desemnează un tip existential. Acesta poate conține o valoare de orice tip concret care respectă protocolul indicat.
Termenul existential vine din simbolul matematic ∃, adică „există”. Știm că în interior există o valoare al cărei tip respectă PaymentMethod, dar nu fixăm acel tip în definiția statică a variabilei.
var selectedMethod: any PaymentMethod = CardPayment()
selectedMethod.pay(amount: 100)
selectedMethod = QRPayment()
selectedMethod.pay(amount: 200)
Variabila rămâne de tip any PaymentMethod, deși conține mai întâi CardPayment, apoi QRPayment. Pentru checkout, asta înseamnă că putem schimba metoda de plată fără să reconstruim ViewModel-ul sau să modificăm parametrii săi generici.
De ce a introdus Swift explicit any?
Înainte de Swift 5.6, puteam scrie:
let method: PaymentMethod = CardPayment()
Acesta era deja un existential, dar sintaxa semăna cu folosirea obișnuită a numelui unui protocol. În propunerea SE-0335: Introduce existential any, autorii explicau motivația astfel:
“The cost of using existential types should not be hidden”
Cu alte cuvinte, costul utilizării tipurilor existential nu ar trebui ascuns. Și nu era vorba doar despre performanță. Vechea sintaxă făcea ca două construcții foarte diferite să pară aproape identice:
func checkout<T: PaymentMethod>(_ method: T)
func checkout(_ method: any PaymentMethod)
În prima declarație, PaymentMethod este o constrângere pentru parametrul generic T. În a doua, parametrul este chiar un existential. Propunerea arăta că această ambiguitate îi determina uneori pe dezvoltatori să aleagă existentials atunci când aveau nevoie de generics. Problemele apăreau mai târziu, când codul depindea de identitatea tipului concret sau de cerințe precum Self și associatedtype.
Prin any, operația de type erasure devine vizibilă la nivelul API-ului. Asta nu înseamnă că runtime-ul uită tipul original. Swift păstrează informațiile necesare pentru a gestiona valoarea; ceea ce pierdem este posibilitatea de a folosi liber identitatea tipului concret în sistemul static de tipuri.
De asemenea, any PaymentMethod nu este același lucru cu Any. Any poate conține aproape orice valoare Swift. any PaymentMethod acceptă doar tipuri care respectă protocolul și ne permite să apelăm cerințele sale fără cast.
some: cum ascundem implementarea unui SDK
Să presupunem că dezvoltăm un SDK de plăți pentru alte echipe. Trebuie să distingem două niveluri: în interiorul SDK-ului controlăm implementarea și îi putem reorganiza tipurile; în afara SDK-ului, utilizatorii văd doar API-ul public.
În exemplul nostru, PaymentMethod este un protocol public, iar CardProcessor este un tip intern (internal) al SDK-ului. Scriem explicit internal pentru claritate, deși acesta este nivelul de acces implicit în Swift.
public protocol PaymentMethod {
func pay(amount: Decimal)
}
internal struct CardProcessor: PaymentMethod {
func pay(amount: Decimal) {
print("Processing card payment")
}
}
SDK-ul trebuie să ofere o funcție publică pentru crearea procesorului de plată implicit.
Varianta 1: returnăm tipul concret
Prima idee este:
public func makePaymentMethod() -> CardProcessor {
CardProcessor()
}
Dar codul nu compilează. O funcție publică nu poate expune un tip de retur internal. Am putea declara CardProcessor drept public:
public struct CardProcessor: PaymentMethod {
public func pay(amount: Decimal) {
print("Processing card payment")
}
}
public func makePaymentMethod() -> CardProcessor {
CardProcessor()
}
Acum compilează, însă CardProcessor a devenit parte din contractul public al SDK-ului. Aplicațiile care îl folosesc pot ajunge să depindă direct de acest tip. Ce se întâmplă dacă, mai târziu, adăugăm logging?
internal struct LoggingPayment<Base: PaymentMethod>: PaymentMethod {
let base: Base
func pay(amount: Decimal) {
print("Payment started")
base.pay(amount: amount)
}
}
Tipul real returnat devine LoggingPayment<CardProcessor>. Dacă vrem să îl scriem în semnătura publică, trebuie să facem public și LoggingPayment. Structura internă a SDK-ului începe să se scurgă în API-ul său.
Varianta 2: returnăm any PaymentMethod
Putem ascunde implementarea în spatele unui existential:
public func makePaymentMethod() -> any PaymentMethod {
LoggingPayment(base: CardProcessor())
}
Utilizatorii SDK-ului nu mai trebuie să cunoască niciunul dintre cele două tipuri interne. Este o soluție perfect validă. Există însă un compromis: rezultatul este un existential, deși fabrica produce întotdeauna un singur tip concret. Facem type erasure fără să avem nevoie ca rezultatul să poată reprezenta tipuri concrete diferite.
Varianta 3: returnăm some PaymentMethod
Aici devine util some:
public func makePaymentMethod() -> some PaymentMethod {
LoggingPayment(base: CardProcessor())
}
Ascundem în continuare tipurile interne ale SDK-ului, dar rezultatul nu este un existential.
some este un cuvânt-cheie Swift care, în poziția tipului returnat, desemnează un opaque result type. Implementarea alege un singur underlying concrete type. Codul apelant nu îi cunoaște numele, dar compilatorul îi păstrează identitatea.
let method = makePaymentMethod()
method.pay(amount: 100)
Clientul SDK-ului nu trebuie să știe că în spate se află LoggingPayment<CardProcessor>. Fără some, cele două opțiuni principale erau să expunem tipul concret sau să returnăm un existential prin any. some oferă a treia variantă: ascundem numele tipului, dar păstrăm faptul că este determinat static.
Există totuși o precizare importantă pentru autorii de SDK-uri: schimbarea underlying type nu este automat sigură pentru compatibilitatea binară. În bibliotecile care folosesc library evolution trebuie respectate regulile ABI și resilience pentru opaque result types.
De ce nu putem returna orice cu some?
Să încercăm o fabrică ce alege metoda de plată după o condiție:
func makePaymentMethod(useQR: Bool) -> some PaymentMethod {
if useQR {
return QRPayment()
}
return CardPayment()
}
Codul nu compilează. QRPayment și CardPayment sunt două tipuri concrete diferite, iar opaque result type al acestei declarații trebuie să aibă un singur underlying type.
Dacă funcția trebuie într-adevăr să aleagă între implementări diferite la runtime, folosim any:
func makePaymentMethod(useQR: Bool) -> any PaymentMethod {
if useQR {
return QRPayment()
}
return CardPayment()
}
Aceasta este diferența practică: some ascunde un tip concret determinat, în timp ce any permite mai multe tipuri prin aceeași interfață.
O analogie simplă: jucării în cutii
Imaginăm un magazin cu mașinuțe, roboți și dinozauri de jucărie. Toate sunt jucării, dar reprezintă tipuri diferite.
Prima cutie este o cutie-surpriză (any). Astăzi poate conține o mașinuță, mâine un robot, iar poimâine un dinozaur. Știm doar că înăuntru este o jucărie. Tipul concret se poate schimba.
A doua este o cutie cu un model secret (some). Producătorul a ales un anumit model, dar nu îi dezvăluie numele. Fiecare cutie din această serie conține o jucărie de același tip. Exemplarele diferă, însă tipul rămâne fix.
Generics seamănă cu o cutie pentru care cumpărătorul alege modelul. Dacă alege un robot, sistemul lucrează cu roboți; dacă alege o mașinuță, lucrează cu mașinuțe.
Ideea nu este doar că o cutie este închisă. Atât some, cât și any ascund tipul concret față de utilizatorul API-ului. Diferența este că some garantează un underlying type, iar any permite schimbarea tipului conținut.
Cum se vede diferența în checkout
Revenim la ecranul de plată. Utilizatorul trebuie să poată trece de la card la QR code. Mai întâi încercăm un ViewModel generic:
final class CheckoutViewModel<Method: PaymentMethod> {
private(set) var selectedMethod: Method
init(selectedMethod: Method) {
self.selectedMethod = selectedMethod
}
func select(_ method: Method) {
selectedMethod = method
}
}
După ce îl creăm astfel:
let viewModel = CheckoutViewModel(
selectedMethod: CardPayment()
)
obținem CheckoutViewModel<CardPayment>. Următorul apel nu este permis:
viewModel.select(QRPayment()) // Compile-time error
Parametrul Method este deja fixat la CardPayment și nu se schimbă pentru acel obiect. Dacă vrem să schimbăm efectiv metoda de plată, un existential se potrivește mai bine:
final class CheckoutViewModel {
private(set) var selectedMethod: any PaymentMethod
init(selectedMethod: any PaymentMethod) {
self.selectedMethod = selectedMethod
}
func select(_ method: any PaymentMethod) {
selectedMethod = method
}
func pay(amount: Decimal) {
selectedMethod.pay(amount: amount)
}
}
let viewModel = CheckoutViewModel(
selectedMethod: CardPayment()
)
viewModel.select(QRPayment())
viewModel.pay(amount: 500)
Aici alegem any pentru că reflectă cerința ecranului, nu pentru că are o sintaxă mai interesantă. some este util la alt nivel, de exemplu în fabrica unui SDK care ascunde procesorul de plată concret.
Ce se întâmplă dacă folosim some în parametri?
Până acum am folosit some în tipul returnat. Dar Swift îl permite și în parametrii unei funcții:
func checkout(using method: some PaymentMethod) {
method.pay(amount: 100)
}
Aici sensul este diferit. În poziția unui parametru, some este o prescurtare pentru generics. Exemplul de mai sus este echivalent, ca semnificație, cu:
func checkout<T: PaymentMethod>(using method: T) {
method.pay(amount: 100)
}
Tipul concret este ales de apelant. Dacă transmitem CardPayment, atunci T este CardPayment; dacă transmitem QRPayment, T devine QRPayment.
De ce nu folosim mereu forma scurtă? Pentru că uneori trebuie să exprimăm o relație între tipurile mai multor parametri. De exemplu, o funcție care primește două metode de plată de același tip:
func compare<T: PaymentMethod>(
_ first: T,
_ second: T
) {
// ...
}
Același T impune același concrete type pentru ambele argumente. Dar dacă scriem:
func compare(
_ first: some PaymentMethod,
_ second: some PaymentMethod
) {
// ...
}
fiecare apariție a lui some introduce un parametru generic separat. Astfel, putem transmite simultan CardPayment și QRPayment. Forma scurtă este comodă pentru cazurile simple, iar generics cu nume sunt utile când trebuie să legăm tipurile între ele.
Ce se întâmplă în interiorul Swift
Acum că știm la ce folosesc some și any, putem înțelege de ce costurile lor pot fi diferite. Să revenim la checkout:
let method: any PaymentMethod = CardPayment()
Am creat un CardPayment, dar l-am păstrat într-o variabilă de tip any PaymentMethod. Mai târziu, aceeași variabilă poate conține QRPayment sau o structură dintr-un SDK extern, cu altă dimensiune și alte reguli de gestionare a memoriei.
Compilatorul nu poate presupune că method va conține mereu CardPayment. Swift trebuie să păstreze atât valoarea, cât și informațiile necesare pentru a lucra corect cu ea. Pentru un existential obișnuit cu value type și un singur protocol, pe o platformă pe 64 de biți, reprezentarea poate fi simplificată astfel:
┌──────────────────────────────┐
│ Value buffer: 3 words │
├──────────────────────────────┤
│ Type metadata pointer │
├──────────────────────────────┤
│ Protocol witness table ptr │
└──────────────────────────────┘
În acest caz avem de obicei cinci machine words, adică 40 de octeți. Nu este însă dimensiunea universală a oricărui any; un class-constrained existential, de exemplu, are altă reprezentare.
Primele trei cuvinte formează un buffer pentru valoare. Dacă structura este suficient de mică și permite stocarea inline, Swift o poate păstra direct în container. Așadar, CardPayment nu ajunge obligatoriu în heap doar pentru că folosim any. Pentru valori mai mari sau incompatibile cu stocarea inline, poate fi necesară stocarea indirectă și, uneori, o alocare suplimentară.
Acum schimbăm valoarea:
var method: any PaymentMethod = CardPayment()
method = QRPayment()
Cum știe Swift că în interior se află acum QRPayment? Prin type metadata, care oferă informații despre tipul concret. Dar cunoașterea tipului nu este suficientă. Când apelăm:
method.pay(amount: 100)
Swift trebuie să găsească implementarea potrivită pentru pay(amount:). Aici intervine Protocol Witness Table (PWT). Ea leagă cerințele protocolului de implementările tipului concret. CardPayment și QRPayment au informații de conformitate distincte, prin care se poate ajunge la metoda corectă.
Dar dacă trebuie să copiem sau să distrugem valoarea, nu să apelăm o metodă? Pentru asta Swift folosește Value Witness Table (VWT), accesibilă prin metadata tipului. Aceasta oferă operații pentru copierea, mutarea și distrugerea valorilor. Pe scurt, PWT ne ajută să apelăm comportamentul definit de protocol, iar VWT să gestionăm valoarea propriu-zisă.
Acum înțelegem de unde pot apărea costurile suplimentare ale unui existential: containerul trebuie să suporte tipuri diferite, iar apelurile prin protocol necesită, în cazul general, un nivel de indirectare. Dar înseamnă asta că generics și some apelează întotdeauna metodele direct? Nu.
Să privim din nou funcția generică:
func checkout<T: PaymentMethod>(
using method: T,
amount: Decimal
) {
method.pay(amount: amount)
}
Swift poate compila o singură implementare generală care primește metadata și witness table pentru tipul T. În acest caz, apelul pay(amount:) poate folosi tot o witness table. Prin urmare, generics nu garantează automat static dispatch.
Există însă o oportunitate de optimizare. Când apelăm:
checkout(using: CardPayment(), amount: 100)
compilatorul poate vedea că T este CardPayment și poate genera o versiune specializată a funcției. Astfel, apelul indirect prin witness table poate deveni un apel direct, iar uneori corpul metodei poate fi inclus prin inlining.
Cu some, underlying type este determinat, chiar dacă numele lui este ascuns apelantului. Acest lucru păstrează oportunitățile de optimizare. Cu any, tipul concret se poate schimba la runtime, așa că este mai greu de stabilit în avans ce implementare va fi apelată. Totuși, optimizatorul poate uneori să identifice tipul din existential și să elimine o parte din overhead.
Așadar, nu este corect să spunem că some este întotdeauna mai rapid decât any. Mai exact, generics și opaque types păstrează mai multă informație despre tipul concret și oferă compilatorului mai multe ocazii de specializare.
Într-un checkout real, timpul unui request de rețea va depăși de regulă cu mult costul unui apel prin witness table. În schimb, într-un hot loop executat de milioane de ori, diferența poate conta și trebuie măsurată. Alegerea inițială ar trebui făcută în funcție de modelul API-ului, nu pe baza unei presupuneri despre viteză.
De ce întâlnim atât de des some View în SwiftUI?
Cel mai cunoscut exemplu este proprietatea body:
struct CheckoutView: View {
var body: some View {
VStack {
Text("Checkout")
Button("Pay") {
// ...
}
}
}
}
Text este un struct, Button este un struct generic, VStack păstrează conținutul printr-un parametru generic, iar modificatorii pot construi noi tipuri care le învelesc pe cele anterioare. Chiar și un ecran simplu poate avea un concrete type foarte complicat.
Dacă ar trebui să scriem manual acel tip în semnătura lui body, codul ar deveni greu de citit și fragil. some View ascunde numele tipului complex, dar îi permite compilatorului să cunoască underlying type. Acest lucru ajută atât la verificarea relațiilor dintre tipuri, cât și la optimizare.
De ce nu putem scrie pur și simplu any View?
Să încercăm:
struct CheckoutView: View {
var body: any View {
Text("Checkout")
}
}
Codul nu satisface cerințele protocolului View. Pare ciudat: Text respectă View, iar any View poate conține un Text.
Problema este că tipul existential any View nu respectă el însuși, în general, protocolul View. Afirmația „în interior se află ceva care respectă View” nu este echivalentă cu „any View respectă View”.
Protocolul View are un associated type numit Body. Pentru ca un tip să respecte View, compilatorul trebuie să poată determina tipul său Body. Dar any View poate conține view-uri diferite, cu tipuri Body diferite. Nu există un singur Body care să descrie toate aceste valori. De aceea, some View nu poate fi înlocuit mecanic cu any View.
Atunci la ce folosește AnyView?
SwiftUI oferă structura AnyView, creată special pentru type erasure. Spre deosebire de any View, AnyView respectă ea însăși protocolul View.
func makePaymentScreen(useQR: Bool) -> AnyView {
if useQR {
return AnyView(Text("Scan QR code"))
}
return AnyView(Button("Pay by card") {
// ...
})
}
În interior pot exista view-uri de tipuri diferite, însă funcția returnează un singur concrete type: AnyView. Totuși, în SwiftUI modern nu trebuie să folosim AnyView pentru orice condiție.
Într-un @ViewBuilder, de obicei putem scrie direct un if:
struct PaymentScreen: View {
let useQR: Bool
var body: some View {
if useQR {
Text("Scan QR code")
} else {
Button("Pay by card") {
// ...
}
}
}
}
De ce funcționează, deși ramurile construiesc view-uri diferite? Pentru că @ViewBuilder transformă condiția într-o structură compusă cu un singur concrete type, de exemplu prin _ConditionalContent. Prin urmare, body continuă să respecte cerința lui some View.
AnyView, în schimb, face type erasure propriu-zis. Apple precizează că, atunci când tipul view-ului din interiorul unui AnyView se schimbă, vechea ierarhie este distrusă și se creează una nouă. De aceea, nu merită să învelim tot ecranul în AnyView fără un motiv concret.
În concluzie, some View ascunde un tip complex, dar determinat; any View este un existential care nu poate îndeplini direct cerința pentru body; iar AnyView este o structură specială pentru type erasure care respectă ea însăși View.