17 ตุลาคม 2569 · 24 นาทีในการอ่าน
some vs any ใน Swift: ต่างกันอย่างไร และควรใช้แบบไหนเมื่อไหร่
หากพัฒนาแอป iOS มาสักพัก คุณคงจำได้ว่าเมื่อก่อนเราใช้ชื่อโปรโตคอลเป็นชนิดของตัวแปร พารามิเตอร์ หรือค่าที่ฟังก์ชันส่งกลับได้ โดยไม่ต้องมีคีย์เวิร์ดเพิ่มเติม
Swift 5.1 เพิ่ม some และ Swift 5.6 เพิ่ม any จากที่เคยเขียนเพียง Protocol เราจึงเห็น some Protocol และ any Protocol มากขึ้น ทั้งคู่ดูเหมือนช่วยซ่อนรายละเอียดการ implement แต่ทำไมต้องมีสองแบบ? ทำไม some คืนค่าหลายชนิดจากฟังก์ชันเดียวกันไม่ได้ ในขณะที่ any ทำได้? และ some เร็วกว่าเสมอจริงหรือไม่?
เริ่มจากตัวอย่างหน้าชำระเงินที่พบในงานจริง จากนั้นดูการออกแบบ SDK และรายละเอียดภายในของ Swift
เริ่มจากโปรโตคอลธรรมดา
สมมติว่าเรากำลังสร้างหน้าชำระเงินที่รองรับบัตรธนาคารและคิวอาร์โค้ด
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 กำหนดสัญญาการทำงาน ส่วน CardPayment และ QRPayment เป็นการ implement สองแบบ ต่อไปสร้างฟังก์ชันชำระเงิน:
func checkout<T: PaymentMethod>(
using method: T,
amount: Decimal
) {
method.pay(amount: amount)
}
นี่คือฟังก์ชัน generic เราใช้พารามิเตอร์ชนิด T โดยกำหนดให้สอดคล้องกับ PaymentMethod แทนการระบุชนิดเฉพาะโดยตรง
checkout(using: CardPayment(), amount: 100)
checkout(using: QRPayment(), amount: 200)
Swift อนุมาน T เป็น CardPayment ในการเรียกครั้งแรก และเป็น QRPayment ในครั้งที่สอง สิ่งสำคัญคือ ฟังก์ชัน generic ไม่ได้แปลงอาร์กิวเมนต์ให้เป็น existential โดยอัตโนมัติ มันยังทำงานกับชนิดเฉพาะ T ที่ผ่านข้อกำหนดของโปรโตคอล และรักษาความเป็นชนิดนั้นไว้
แล้วถ้าต้องเก็บการ implement หลายแบบไว้ภายใต้อินเทอร์เฟซเดียวกันล่ะ?
any: เมื่อชนิดเฉพาะเปลี่ยนได้
ในหน้าชำระเงิน ผู้ใช้เลือกวิธีชำระเงินได้ เราจึงต้องการเก็บตัวเลือกต่าง ๆ ไว้ในอาร์เรย์เดียว:
let paymentMethods: [any PaymentMethod] = [
CardPayment(),
QRPayment()
]
อาร์เรย์ของ Swift มีชนิดของสมาชิกเพียงชนิดเดียว แต่สอง struct นี้เป็นคนละชนิด any PaymentMethod ช่วยให้เก็บทั้งคู่ไว้ด้วยกันได้
any คือคีย์เวิร์ดของ Swift ที่ใช้ระบุชนิดแบบ existential ซึ่งสามารถเก็บค่าจากชนิดเฉพาะใดก็ได้ที่สอดคล้องกับโปรโตคอลนั้น คำว่า existential มาจากสัญลักษณ์คณิตศาสตร์ ∃ ที่แปลว่า “มีอยู่” เรารู้ว่าภายในมีค่าจากชนิดที่สอดคล้องกับโปรโตคอล แต่ไม่ได้ตรึงชนิดนั้นไว้กับชนิดแบบ static ของตัวแปร
var selectedMethod: any PaymentMethod = CardPayment()
selectedMethod.pay(amount: 100)
selectedMethod = QRPayment()
selectedMethod.pay(amount: 200)
ตัวแปรยังมีชนิด any PaymentMethod เหมือนเดิม แม้ค่าข้างในจะเปลี่ยนจาก CardPayment เป็น QRPayment นี่คือสิ่งที่เหมาะกับการสลับวิธีชำระเงิน โดยไม่ต้องเปลี่ยนพารามิเตอร์ generic ของ ViewModel
ทำไม Swift จึงบังคับให้เขียน any ชัดเจน?
ก่อน Swift 5.6 เราเขียนแบบนี้ได้:
let method: PaymentMethod = CardPayment()
จริง ๆ แล้วนี่เป็น existential อยู่แล้ว แต่รูปแบบดูเหมือนการใช้ชื่อโปรโตคอลทั่วไป ในข้อเสนอ SE-0335: Introduce existential any มีประโยคสำคัญว่า:
“The cost of using existential types should not be hidden”
หมายความว่า ค่าใช้จ่ายของการใช้ existential ไม่ควรถูกซ่อนไว้ และค่าใช้จ่ายที่ว่าก็ไม่ใช่แค่เรื่องความเร็ว เพราะไวยากรณ์เดิมทำให้สองแนวคิดต่อไปนี้ดูคล้ายกันเกินไป:
func checkout<T: PaymentMethod>(_ method: T)
func checkout(_ method: any PaymentMethod)
บรรทัดแรกใช้ PaymentMethod เป็นข้อจำกัดของ generic T ส่วนบรรทัดที่สองรับ existential โดยตรง ความคลุมเครือนี้ทำให้บางครั้งนักพัฒนาเลือก existential ทั้งที่จริงต้องการ generics แล้วจึงพบปัญหาเมื่อโค้ดต้องอาศัยความเป็นชนิดเฉพาะ หรือข้อกำหนดอย่าง Self และ associatedtype
คีย์เวิร์ด any จึงทำให้การลบชนิด (type erasure) มองเห็นได้จากฝั่ง API แต่ไม่ได้หมายความว่าช่วงเวลารัน (runtime) ลืมชนิดจริง Swift ยังเก็บข้อมูลที่จำเป็นไว้ เพียงแต่โค้ดที่เรียกใช้ไม่สามารถพึ่งพาชนิดนั้นในระบบ static type ได้อย่างอิสระ
อีกอย่าง any PaymentMethod ไม่เหมือน Any เพราะ Any เก็บค่า Swift ได้แทบทุกชนิด แต่ any PaymentMethod รับเฉพาะค่าที่สอดคล้องกับโปรโตคอล และเรียกเมธอดของโปรโตคอลได้โดยไม่ต้อง cast
some: ซ่อนรายละเอียดภายในของ SDK
สมมติว่าเรากำลังพัฒนา SDK สำหรับการชำระเงินให้ทีมอื่นนำไปใช้ ต้องแยกสองส่วนให้ชัด: ภายใน SDK เราควบคุมการ implement และปรับโครงสร้างได้ ส่วน ภายนอก SDK ผู้ใช้เห็นเพียง public API
ในตัวอย่างนี้ PaymentMethod เป็น public protocol ส่วน CardProcessor เป็น ชนิดภายใน SDK (internal) เราเขียน internal ให้เห็นชัด แม้ Swift จะใช้ระดับการเข้าถึงนี้เป็นค่าเริ่มต้นอยู่แล้ว
public protocol PaymentMethod {
func pay(amount: Decimal)
}
internal struct CardProcessor: PaymentMethod {
func pay(amount: Decimal) {
print("Processing card payment")
}
}
SDK ต้องมี public factory สำหรับสร้างตัวประมวลผลการชำระเงินเริ่มต้น
ทางเลือกที่ 1: คืนชนิดเฉพาะโดยตรง
วิธีแรกที่นึกถึงคือ:
public func makePaymentMethod() -> CardProcessor {
CardProcessor()
}
แต่โค้ดนี้คอมไพล์ไม่ผ่าน เพราะฟังก์ชัน public เปิดเผยชนิดที่เป็น internal เราอาจเปลี่ยน CardProcessor เป็น public:
public struct CardProcessor: PaymentMethod {
public func pay(amount: Decimal) {
print("Processing card payment")
}
}
public func makePaymentMethod() -> CardProcessor {
CardProcessor()
}
แบบนี้คอมไพล์ได้ แต่ชื่อและความสามารถที่ public ของ CardProcessor กลายเป็นส่วนหนึ่งของสัญญาสาธารณะ ผู้ใช้ SDK อาจอ้างอิงชนิดนี้โดยตรง วันหนึ่งถ้าเราเพิ่มระบบบันทึก log ล่ะ?
internal struct LoggingPayment<Base: PaymentMethod>: PaymentMethod {
let base: Base
func pay(amount: Decimal) {
print("Payment started")
base.pay(amount: amount)
}
}
ตอนนี้ชนิดจริงที่คืนคือ LoggingPayment<CardProcessor> ถ้าจะระบุชนิดนี้ใน signature ที่ public ก็ต้องเปิดเผย LoggingPayment อีกตัว รายละเอียดภายใน SDK จึงค่อย ๆ รั่วออกไปเป็น public API
ทางเลือกที่ 2: คืน any PaymentMethod
เราซ่อนรายละเอียดการ implement ด้วย existential ได้:
public func makePaymentMethod() -> any PaymentMethod {
LoggingPayment(base: CardProcessor())
}
ผู้ใช้ SDK ไม่ต้องรู้จักสอง struct ภายในอีกต่อไป นี่เป็นทางเลือกที่ถูกต้อง แต่ผลลัพธ์เป็น existential ทั้งที่ factory สร้างชนิดเฉพาะเดียวเสมอ เราทำ type erasure โดยไม่ได้ต้องการให้ค่าผลลัพธ์เปลี่ยนชนิดได้จริง ๆ
ทางเลือกที่ 3: คืน some PaymentMethod
นี่คือจุดที่ some มีประโยชน์:
public func makePaymentMethod() -> some PaymentMethod {
LoggingPayment(base: CardProcessor())
}
เรายังซ่อนชนิดภายใน SDK แต่ผลลัพธ์ไม่ใช่ existential
some คือคีย์เวิร์ดของ Swift ซึ่งในตำแหน่งชนิดที่คืนกลับหมายถึง opaque result type ส่วนที่ implement เป็นฝ่ายเลือกชนิด underlying เพียงชนิดเดียว ผู้เรียกไม่เห็นชื่อชนิดนั้น แต่คอมไพเลอร์ยังรักษาความเป็นชนิดนั้นไว้
let method = makePaymentMethod()
method.pay(amount: 100)
ผู้ใช้ SDK ไม่จำเป็นต้องรู้ว่าภายในคือ LoggingPayment<CardProcessor> หากไม่มี some เราต้องเลือกระหว่างเปิดเผยชนิดเฉพาะกับคืน existential ผ่าน any ส่วน some ให้ทางเลือกที่สาม คือซ่อนชื่อชนิดโดยยังคงความแน่นอนของชนิดไว้
มีข้อควรระวังสำหรับ SDK ที่ใช้ library evolution: การเปลี่ยนชนิด underlying ไม่ได้ปลอดภัยต่อความเข้ากันได้ของไบนารีเสมอ ต้องพิจารณากฎ ABI และ resilience ด้วย
ทำไม some จึงคืนค่าอะไรก็ได้ไม่ได้?
ลองให้ factory เลือกวิธีชำระเงินตามเงื่อนไข:
func makePaymentMethod(useQR: Bool) -> some PaymentMethod {
if useQR {
return QRPayment()
}
return CardPayment()
}
โค้ดนี้คอมไพล์ไม่ผ่าน เพราะ QRPayment กับ CardPayment เป็นคนละชนิดเฉพาะ แต่ opaque result type ของการประกาศนี้ต้องมีชนิด underlying เดียว
ถ้าต้องเลือกการ implement คนละแบบระหว่าง runtime ให้ใช้ any:
func makePaymentMethod(useQR: Bool) -> any PaymentMethod {
if useQR {
return QRPayment()
}
return CardPayment()
}
ดังนั้น some ซ่อนชนิดที่แน่นอนหนึ่งชนิด ส่วน any รองรับหลายชนิดผ่านอินเทอร์เฟซเดียว
เปรียบเทียบง่าย ๆ: ของเล่นในกล่อง
ลองนึกถึงร้านขายของเล่นที่มีรถของเล่น หุ่นยนต์ และไดโนเสาร์ ทุกอย่างเป็นของเล่นเหมือนกัน แต่เป็นคนละชนิด
กล่องแรกคือกล่องสุ่ม (any) วันนี้อาจได้รถ พรุ่งนี้ได้หุ่นยนต์ วันถัดไปได้ไดโนเสาร์ เรารู้เพียงว่าข้างในเป็นของเล่น ชนิดจริงเปลี่ยนได้
กล่องที่สองคือกล่องของรุ่นลับ (some) ผู้ผลิตเลือกรุ่นหนึ่งไว้ แต่ไม่บอกชื่อรุ่นให้ลูกค้ารู้ ทุกกล่องของสินค้านี้มีของเล่นชนิดเดียวกัน แม้จะเป็นคนละชิ้นก็ตาม ชนิดถูกกำหนดแล้ว เพียงแต่ซ่อนชื่อไว้
ส่วน generics เหมือนกล่องที่ลูกค้าเป็นคนเลือกรุ่นเอง หากเลือกหุ่นยนต์ ระบบก็ทำงานกับหุ่นยนต์ หากเลือกรถก็ทำงานกับรถ
จุดสำคัญไม่ใช่แค่กล่องปิดอยู่ เพราะทั้ง some และ any ต่างก็ซ่อนชนิดเฉพาะไว้ แต่ some รับประกันชนิด underlying เดียว ส่วน any อนุญาตให้ชนิดภายในเปลี่ยนได้
ความต่างนี้ส่งผลต่อหน้าชำระเงินอย่างไร
กลับมาที่หน้าชำระเงิน สมมติว่าผู้ใช้สลับระหว่างบัตรกับคิวอาร์โค้ด เราอาจเริ่มจาก ViewModel แบบ generic:
final class CheckoutViewModel<Method: PaymentMethod> {
private(set) var selectedMethod: Method
init(selectedMethod: Method) {
self.selectedMethod = selectedMethod
}
func select(_ method: Method) {
selectedMethod = method
}
}
เมื่อสร้างออบเจ็กต์ดังนี้:
let viewModel = CheckoutViewModel(
selectedMethod: CardPayment()
)
ชนิดที่ได้คือ CheckoutViewModel<CardPayment> จึงเรียกแบบนี้ไม่ได้:
viewModel.select(QRPayment()) // Compile-time error
เพราะ Method ถูกกำหนดเป็น CardPayment แล้วและไม่เปลี่ยนในออบเจ็กต์เดิม ถ้าต้องการสลับวิธีชำระเงินจริง ๆ ใช้ existential จะตรงกับโจทย์กว่า:
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)
ตรงนี้เราเลือก any ไม่ใช่เพราะรูปแบบการเขียน แต่เพราะความต้องการของหน้าจอ ส่วน some เหมาะกับอีกชั้นหนึ่ง เช่น factory ของ SDK ที่ซ่อนตัวประมวลผลจริง
แล้ว some ในพารามิเตอร์ล่ะ?
ที่ผ่านมาเราใช้ some ในชนิดที่คืนกลับ แต่ Swift อนุญาตให้ใช้ในพารามิเตอร์ได้ด้วย:
func checkout(using method: some PaymentMethod) {
method.pay(amount: 100)
}
ตรงนี้มีความหมายต่างออกไป some ในพารามิเตอร์เป็นรูปแบบย่อของ generics ตัวอย่างข้างต้นมีความหมายเทียบเท่ากับ:
func checkout<T: PaymentMethod>(using method: T) {
method.pay(amount: 100)
}
ชนิดเฉพาะถูกเลือกโดยโค้ดที่เรียกใช้ ถ้าส่ง CardPayment ก็ได้ T เป็น CardPayment ถ้าส่ง QRPayment ก็เป็น QRPayment
แล้วทำไมไม่ใช้รูปแบบสั้นเสมอ? เพราะบางครั้งเราต้องกำหนดความสัมพันธ์ระหว่างชนิดของหลายพารามิเตอร์ เช่น ฟังก์ชันที่รับวิธีชำระเงินสองค่าที่ต้องเป็นชนิดเดียวกัน:
func compare<T: PaymentMethod>(
_ first: T,
_ second: T
) {
// ...
}
T ตัวเดียวกันบังคับให้ทั้งสองอาร์กิวเมนต์มีชนิดเฉพาะเดียวกัน แต่ถ้าเขียนแบบนี้:
func compare(
_ first: some PaymentMethod,
_ second: some PaymentMethod
) {
// ...
}
some แต่ละตำแหน่งสร้างพารามิเตอร์ generic แยกกัน จึงส่ง CardPayment และ QRPayment มาพร้อมกันได้ รูปแบบย่อเหมาะกับกรณีง่าย ๆ ส่วน generics ที่มีชื่อ เหมาะเมื่อเราต้องเชื่อมความสัมพันธ์ของชนิด
ภายใน Swift ทำงานอย่างไร
เมื่อเข้าใจการใช้งานแล้ว ลองดูว่าทำไม some และ any อาจมีค่าใช้จ่ายต่างกัน กลับไปที่หน้าชำระเงิน:
let method: any PaymentMethod = CardPayment()
เราสร้าง CardPayment แต่เก็บไว้ในตัวแปรชนิด any PaymentMethod ต่อมาเราอาจเปลี่ยนเป็น QRPayment หรือ struct อื่นที่มีขนาดและวิธีจัดการหน่วยความจำต่างกัน
คอมไพเลอร์จึงไม่สามารถสมมติได้ว่าตัวแปรนี้เป็น CardPayment ตลอดไป Swift ต้องเก็บทั้งค่าและข้อมูลที่ช่วยจัดการค่านั้น สำหรับ existential ของ value type ที่สอดคล้องกับโปรโตคอลเดียว บนระบบ 64 บิต เราอธิบายโครงสร้างโดยย่อได้ดังนี้:
┌──────────────────────────────┐
│ Value buffer: 3 words │
├──────────────────────────────┤
│ Type metadata pointer │
├──────────────────────────────┤
│ Protocol witness table ptr │
└──────────────────────────────┘
โดยทั่วไปขนาดคือ 5 machine words หรือ 40 ไบต์ แต่ไม่ใช่ขนาดของ any ทุกกรณี เช่น existential ที่จำกัดให้เป็น class มี representation ต่างออกไป
สาม word แรกเป็นบัฟเฟอร์สำหรับเก็บค่า หาก struct มีขนาดเล็กและเหมาะกับการเก็บแบบ inline Swift ก็เก็บไว้ในคอนเทนเนอร์ได้เลย ดังนั้นการใช้ any ไม่ได้ทำให้ CardPayment ต้องถูกจัดสรรใน heap เสมอ ถ้าค่าใหญ่เกินไปหรือเก็บแบบ inline ไม่ได้ ก็อาจต้องใช้การเก็บทางอ้อม และบางครั้งต้องจัดสรรหน่วยความจำเพิ่มเติม
ลองเปลี่ยนค่าข้างใน:
var method: any PaymentMethod = CardPayment()
method = QRPayment()
Swift รู้ได้อย่างไรว่าตอนนี้เป็น QRPayment? มันใช้ type metadata ซึ่งให้ข้อมูลเกี่ยวกับชนิดเฉพาะของค่า แต่การรู้ชื่อชนิดยังไม่พอ เมื่อเรียกเมธอด:
method.pay(amount: 100)
Swift ต้องหาการ implement ของ pay(amount:) ให้ถูกต้อง ตรงนี้มี Protocol Witness Table (PWT) ซึ่งเชื่อมข้อกำหนดของโปรโตคอลเข้ากับการ implement ของชนิดนั้น CardPayment และ QRPayment จึงมีข้อมูลการสอดคล้อง (conformance) ของตัวเอง
แล้วถ้าต้องคัดลอกหรือทำลายค่า แทนที่จะเรียกเมธอดล่ะ? งานนี้ใช้ Value Witness Table (VWT) ซึ่งเข้าถึงผ่าน metadata และให้คำสั่งสำหรับคัดลอก ย้าย และทำลายค่า พูดง่าย ๆ PWT ช่วยเรียกพฤติกรรมที่โปรโตคอลกำหนด ส่วน VWT ช่วยจัดการตัวค่า
จึงเห็นได้ว่า existential มีงานเพิ่มเติม: ต้องรองรับค่าหลายชนิด และโดยทั่วไปต้องเรียกเมธอดผ่านข้อมูลการสอดคล้อง แต่ไม่ได้หมายความว่า generics หรือ some จะเรียกเมธอดโดยตรงเสมอไป
ลองดูฟังก์ชัน generic อีกครั้ง:
func checkout<T: PaymentMethod>(
using method: T,
amount: Decimal
) {
method.pay(amount: amount)
}
Swift อาจคอมไพล์ฟังก์ชันนี้เป็นเวอร์ชันทั่วไป ซึ่งยังต้องรับ metadata และ witness table เพื่อเรียก pay(amount:) ดังนั้น generics ไม่ได้รับประกัน static dispatch โดยอัตโนมัติ
อย่างไรก็ตาม เมื่อเราเรียก:
checkout(using: CardPayment(), amount: 100)
คอมไพเลอร์อาจทราบว่า T คือ CardPayment และทำ specialization เพื่อสร้างเวอร์ชันที่เหมาะกับชนิดนี้ การเรียกผ่าน witness table จึงอาจถูกแทนด้วยการเรียกตรง และบางครั้งทำ inlining ได้
สำหรับ some ชนิด underlying ถูกกำหนดไว้ แม้ชื่อจะถูกซ่อนจากผู้เรียก จึงยังมีโอกาสให้คอมไพเลอร์ปรับแต่งได้ ส่วน any อาจมีชนิดเฉพาะต่างกันระหว่าง runtime จึงทำให้การตัดสินใจล่วงหน้ายากกว่า แต่ตัวปรับแต่งก็อาจมองทะลุ existential และลดค่าใช้จ่ายเพิ่มเติมได้ในบางกรณี
ดังนั้นไม่ควรสรุปว่า some เร็วกว่า any เสมอไป ควรพูดว่า generics และ opaque types เก็บข้อมูลเรื่องชนิดไว้มากกว่า จึงเปิดโอกาสให้ทำ specialization ได้มากกว่า
สำหรับหน้าชำระเงิน เวลาที่ใช้ส่งคำขอผ่านเครือข่ายมักมากกว่าค่าใช้จ่ายของการเรียกเมธอดผ่าน witness table หลายเท่า แต่ในลูปที่ทำงานหนัก (hot loop) และวนหลายล้านครั้ง ความต่างอาจมีผลและควรวัดผลจริง ทางเลือกแรกจึงควรขึ้นอยู่กับการออกแบบ API ไม่ใช่การเดาเรื่องความเร็ว
ทำไม SwiftUI จึงใช้ some View บ่อยมาก
ตัวอย่างที่คุ้นเคยที่สุดคือพร็อพเพอร์ตี้ body ของ SwiftUI:
struct CheckoutView: View {
var body: some View {
VStack {
Text("Checkout")
Button("Pay") {
// ...
}
}
}
}
Text เป็น struct, Button เป็น struct แบบ generic, VStack เก็บเนื้อหาผ่านพารามิเตอร์ generic และ modifier ต่าง ๆ ก็สร้างชนิดใหม่ที่ห่อ view เดิมไว้ แม้หน้าจอเล็ก ๆ ก็อาจมีชนิดเฉพาะที่ซับซ้อนมาก
หากต้องเขียนชนิดเต็มด้วยมือ โค้ดจะอ่านยากและเปราะบาง some View จึงช่วยซ่อนชื่อชนิดที่ซับซ้อนใน signature ขณะที่คอมไพเลอร์ยังรู้ชนิด underlying ที่แน่นอน นอกจากอ่านง่ายแล้ว ยังช่วยให้ตรวจสอบความสัมพันธ์ของชนิดและปรับแต่งได้
ทำไมใช้ any View แทนไม่ได้?
ลองเปลี่ยน body เป็น:
struct CheckoutView: View {
var body: any View {
Text("Checkout")
}
}
โค้ดนี้ไม่เป็นไปตามข้อกำหนดของ View ทั้งที่ Text สอดคล้องกับ View และ any View สามารถเก็บ Text ได้
เหตุผลคือ ชนิดแบบ existential any View ไม่ได้สอดคล้องกับ View เองโดยทั่วไป การบอกว่า “ข้างในเป็นค่าที่สอดคล้องกับ View” ไม่เหมือนกับ “ตัว any View สอดคล้องกับ View”
โปรโตคอล View มี associated type ชื่อ Body เมื่อชนิดหนึ่งสอดคล้องกับ View คอมไพเลอร์ต้องกำหนด Body ของชนิดนั้นได้ แต่ any View อาจเก็บ view หลายชนิดซึ่งมี Body ต่างกัน จึงไม่มี Body เดียวที่ใช้แทนทุกค่าได้ นี่คือเหตุผลที่ไม่สามารถสลับ some View เป็น any View ตรง ๆ
แล้ว AnyView มีไว้ทำอะไร?
SwiftUI มี struct ชื่อ AnyView สำหรับทำ type erasure โดยเฉพาะ และต่างจาก any View ตรงที่ AnyView เองสอดคล้องกับ View
func makePaymentScreen(useQR: Bool) -> AnyView {
if useQR {
return AnyView(Text("Scan QR code"))
}
return AnyView(Button("Pay by card") {
// ...
})
}
แม้ภายในจะมี view ต่างชนิดกัน แต่ฟังก์ชันคืนชนิดเฉพาะเดียวคือ AnyView อย่างไรก็ตาม SwiftUI สมัยใหม่ไม่จำเป็นต้องใช้ AnyView สำหรับทุกเงื่อนไข
เมื่อมี if อยู่ภายใน @ViewBuilder เรามักเขียนแบบธรรมดาได้:
struct PaymentScreen: View {
let useQR: Bool
var body: some View {
if useQR {
Text("Scan QR code")
} else {
Button("Pay by card") {
// ...
}
}
}
}
ทำไมสองสาขาจึงคืน view คนละชนิดได้ ทั้งที่ body เป็น some View? เพราะ @ViewBuilder แปลงเงื่อนไขให้กลายเป็นโครงสร้างประกอบที่มีชนิดเฉพาะเดียว เช่นผ่าน _ConditionalContent ดังนั้นเงื่อนไขของ opaque result type ยังเป็นจริง
ในทางกลับกัน AnyView ทำ type erasure จริง Apple ระบุว่าเมื่อชนิดของ view ภายใน AnyView เปลี่ยน ลำดับชั้นของ view เดิมจะถูกทำลายและสร้างใหม่ จึงไม่ควรห่อทั้งหน้าจอด้วย AnyView โดยไม่มีเหตุผล
สรุปคือ some View ซ่อนชนิดที่ซับซ้อนแต่แน่นอน, any View เป็น existential ที่ใช้แทน body โดยตรงไม่ได้ และ AnyView เป็น struct สำหรับ type erasure ที่สอดคล้องกับ View ด้วยตัวเอง