Opaque Type (ชนิดที่ไม่โปร่งใส) เป็นกลไกของ Swift ที่ทำให้ฟังก์ชันสามารถส่งค่ากลับเป็นชนิดหนึ่งได้ โดยไม่เปิดเผยชนิดที่แท้จริงให้โค้ดผู้เรียก ตัวอย่างที่รู้จักมากที่สุดคือคีย์เวิร์ด some ในชนิดส่งกลับ: some View ใน SwiftUI หมายถึง "ส่งกลับชนิดที่สอดคล้องกับ View แต่เป็นชนิดไหน — เป็นรายละเอียดของการ implement" Opaque type รักษาเอกลักษณ์ของชนิด (ต่างจากการใช้ protocol เป็นชนิด) ซึ่งทำให้คอมไพเลอร์เพิ่มประสิทธิภาพโค้ดได้ และรับประกันความสม่ำเสมอของชนิดส่งกลับ จากข้อมูลของ Swift Book, 2025 opaque types แก้ปัญหาของ protocol ที่มี associated types โดยทำให้สามารถส่งค่าของ protocol เหล่านั้นกลับจากฟังก์ชันได้
หัวข้อสำคัญ
Opaque Type เป็นชนิดส่งกลับที่ประกาศด้วยคีย์เวิร์ด some ซึ่งซ่อนการ implement จริงจากโค้ดผู้เรียก ฝ่ายผู้เรียกจะรู้เพียงว่าค่าที่ส่งกลับสอดคล้องกับ protocol หนึ่งๆ แต่ไม่รู้ว่าชนิดที่แท้จริงเบื้องหลัง some คืออะไร ในขณะเดียวกันคอมไพเลอร์รู้ชนิดที่แท้จริง และใช้มันสำหรับ static dispatch และการเพิ่มประสิทธิภาพ
ก่อนที่ opaque types จะปรากฏใน Swift 5.1 (SE-0244) เป็นไปไม่ได้ที่จะส่งกลับ protocol ที่มี associated types จากฟังก์ชันโดยไม่มี boxing-wrapper ตัวอย่างเช่น protocol Equatable มี associated type และฟังก์ชันไม่สามารถส่งกลับ Equatable ธรรมดาได้ — คอมไพเลอร์จะแสดงข้อผิดพลาด "protocol can only be used as a generic constraint" Opaque type แก้ปัญหานี้ได้
func makeInt() -> some Equatable {
return 42
}
func makeString() -> some Equatable {
return "Hello"
}
// คอมไพเลอร์รู้ว่า makeInt ส่งกลับ Int
// makeInt() == makeString() — ❌ ข้อผิดพลาด ชนิดต่างกัน
ฟังก์ชันทั้งสองส่งกลับ some Equatable แต่ ชนิดที่แท้จริง ต่างกัน: Int และ String การพยายามเปรียบเทียบด้วย == จะทำให้เกิดข้อผิดพลาดตอนคอมไพล์ เพราะ opaque type รับประกันว่าจากการเรียกหนึ่งครั้งจะส่งกลับชนิดเดียวกัน แต่ไม่ใช่ระหว่างฟังก์ชันที่ต่างกัน นี่คือฟีเจอร์ ไม่ใช่บั๊ก: opaque type รักษาเอกลักษณ์ของชนิดในจุดที่ protocol เป็นชนิด (any Equatable) ทำให้เอกลักษณ์นั้นหายไป
Generic และ Opaque Type เป็นสองด้านของเหรียญเดียวกัน Generic อนุญาตให้โค้ดผู้เรียกเลือกชนิดได้ ในขณะที่ opaque type อนุญาตให้ฟังก์ชันซ่อนชนิดจากโค้ดผู้เรียก ความแตกต่างคือทิศทางของการควบคุม
| คุณลักษณะ | Generic | Opaque some |
|---|---|---|
| ใครเลือกชนิด | โค้ดผู้เรียก | ฟังก์ชัน/เมธอด |
| เอกลักษณ์ของชนิด | ถูกคงไว้ (เสถียร) | ถูกคงไว้ (เสถียร) |
| จำนวนสาขา return | หนึ่ง (ผ่าน generic) | ชนิดเดียวกันในทุกสาขา |
| การใช้งาน | อัลกอริทึม, โครงสร้างข้อมูล | SwiftUI, เมธอด factory |
ในฟังก์ชัน generic caller เป็นผู้ตัดสินใจว่าจะใช้ชนิดใด ฟังก์ชันต้องทำงานกับ T ใดๆ ที่เป็นไปตามข้อจำกัด สำหรับ opaque type caller ไม่รู้ชนิดที่แท้จริง — การตัดสินใจเป็นของการ implement
// Generic: caller เลือกชนิด
func identity<T>(_ value: T) -> T { value }
let x: Int = identity(42)
// Opaque: ฟังก์ชันซ่อนชนิด
func makeSomeEquatable() -> some Equatable { 42 }
let y = makeSomeEquatable()
การเลือกระหว่าง generic และ opaque type ขึ้นอยู่กับ เจตนา หากโค้ดผู้เรียกต้องเลือกชนิด — ใช้ generic หากฟังก์ชันต้องซ่อนรายละเอียดการ implement — ใช้ some SwiftUI เลือก some View เพราะ body ต้องยืดหยุ่นภายใน แต่เสถียรภายนอก
some เป็นคีย์เวิร์ดของ Swift ที่นำมาใช้ใน Swift 5.1 (SE-0244) มันใช้ในตำแหน่งส่งกลับเพื่อประกาศ opaque type รวมถึงในพารามิเตอร์ (SE-0341) และคุณสมบัติ some รับประกันว่าชนิดที่แท้จริงเสถียรและคอมไพเลอร์รู้จัก แต่ซ่อนจากโค้ดภายนอก
ตั้งแต่ Swift 5.7 เป็นต้นไป some สามารถใช้ได้ไม่เพียงแต่ในตำแหน่งส่งกลับ แต่ยังในพารามิเตอร์ some Equatable ในพารามิเตอร์หมายถึง "ฟังก์ชันนี้รับชนิด Equatable ใดก็ได้ แต่การเรียกทั้งหมดภายใน body เดียวกันเห็นชนิดเดียวกัน"
func areEqual(_ a: some Equatable, _ b: some Equatable) -> Bool {
// a และ b — ชนิดต่างกันได้, == จะทำงานตรงๆ ไม่ได้
return isEqual(a, b)
}
func isEqual<T: Equatable>(_ a: T, _ b: T) -> Bool {
return a == b
}
การใช้ some ในพารามิเตอร์ให้ไวยากรณ์ที่กระชับกว่า
any เป็นคีย์เวิร์ดของ Swift 5.6+ สำหรับประกาศชนิด existential อย่างชัดเจน (protocol เป็นชนิด) ต่างจาก some any ลบเอกลักษณ์ของชนิด: คอมไพเลอร์ไม่รู้ว่าชนิดที่แท้จริงใดซ่อนอยู่เบื้องหลัง protocol สิ่งนี้ให้ความยืดหยุ่น (เก็บชนิดต่างกันใน array เดียวกันได้) แต่ต้องแลกกับประสิทธิภาพ
some — polymorphism แบบ static: คอมไพเลอร์รู้ชนิดที่แท้จริง ใช้ direct dispatch และ inline โค้ดได้ any — polymorphism แบบ dynamic: ใช้ตาราง virtual methods (existential container) ซึ่งเพิ่ม indirection
protocol Drawable {
func draw()
}
// some: รู้ชนิดแบบ static
func makeDrawable() -> some Drawable {
return Circle() // ชนิดส่งกลับเดียว
}
// any: dynamic, เก็บชนิดต่างกันได้
var shapes: [any Drawable] = [Circle(), Square()]
shapes.append(Triangle())
การเลือกระหว่าง some และ any คือการประนีประนอมระหว่าง ประสิทธิภาพ และความยืดหยุ่น Some เร็วกว่าแต่จำกัดให้เหลือการ implement เดียว Any ยืดหยุ่นกว่า (ผสมชนิดได้) แต่ช้ากว่าเพราะ dynamic dispatch ใน SwiftUI สำหรับ body มักใช้ some View เสมอ เพราะ body ของแต่ละ View เป็นชนิดที่แท้จริงเดียว
Opaque Type แก้ปัญหาพื้นฐานของ Swift: protocol ที่มี associated types (PAT) ไม่สามารถใช้เป็นชนิดโดยตรงได้ ฟังก์ชันไม่สามารถส่งกลับแค่ Collection ได้ — คอมไพเลอร์ต้องการให้ระบุ Element some Collection แก้ไขปัญหานี้โดยซ่อน associated type
หากไม่มี opaque type ในการส่งกลับ Collection จะต้องใช้ชนิดที่แท้จริง (Array<Int>) หรือการลบชนิด (AnyCollection<Int>) some Collection ให้ทางสายกลาง: คอมไพเลอร์รู้การ implement ที่แท้จริง โค้ดผู้เรียก — ไม่รู้
func makeReversedCollection<T>(
of array: [T]
) -> some Collection {
return array.reversed()
}
let result = makeReversedCollection(of: [1, 2, 3])
// result — ReversedCollection>, hidden from caller
for item in result {
print(item)
}
result วนซ้ำได้ แต่ไม่สามารถเข้าถึงคุณสมบัติของ ReversedCollection ได้ โดยตรง สิ่งนี้ปกป้อง encapsulation: หากภายหลังเปลี่ยน reversed() เป็นเมธอดอื่นที่มีการ implement ต่างกัน โค้ดผู้เรียกจะไม่พัง opaque type ให้อิสระในการเปลี่ยนการ implement โดยไม่เปลี่ยน API
some View คือการใช้ opaque type ที่รู้จักมากที่สุด ทุก View ใน SwiftUI ประกาศ body เป็น some View นั่นหมายความว่า body ส่งกลับชนิด View ที่แท้จริงบางชนิด แต่ผู้พัฒนาไม่ต้องคิดว่ามันคืออะไร — TupleView, Group, ModifiedContent หรือชนิดอื่นจาก framework
หากไม่มี opaque type body จะต้องส่งกลับชนิดที่แท้จริง เช่น ModifiedContent<Button<Text>, Padding> ซึ่งไม่ปฏิบัติได้จริง some View ซ่อนความซับซ้อนนี้ คอมไพเลอร์อนุมานชนิดที่แท้จริงของ body โดยอัตโนมัติตอนคอมไพล์
struct ContentView: View {
var body: some View {
VStack {
Text("สวัสดี")
.font(.title)
Button("แตะฉัน") {
print("ถูกแตะ")
}
}
.padding()
}
}
คอมไพเลอร์อนุมาน body เป็น ModifiedContent<VStack<TupleView<(Text, Button<Text>)>>, Padding> ผู้พัฒนาเห็น some View หากเปลี่ยน layout จาก VStack เป็น HStack คอมไพเลอร์จะอนุมานชนิดใหม่โดยอัตโนมัติ — ไม่ต้องแก้ด้วยมือ นี่คือเวทมนตร์ของ opaque type: ผู้พัฒนาโฟกัสที่ตรรกะของอินเทอร์เฟซ ไม่ใช่ชนิดของการประกอบ
คำถามที่พบบ่อย
Opaque Type เป็นชนิดที่ประกาศด้วยคีย์เวิร์ด some ซึ่งซ่อนการ implement จริงจากโค้ดผู้เรียก คอมไพเลอร์รู้ชนิดที่แท้จริง แต่ผู้พัฒนาที่ใช้ฟังก์ชันจะเห็นเพียง protocol
some เป็น opaque type ที่มีเอกลักษณ์แบบ static: คอมไพเลอร์รู้ชนิดที่แท้จริง any เป็น existential type ที่มี dynamic dispatch: เอกลักษณ์ของชนิดถูกลบ Some มีประสิทธิภาพกว่า any ยืดหยุ่นกว่า
some View ซ่อนชนิดที่แท้จริงอันซับซ้อนของ body ซึ่งคอมไพเลอร์อนุมานโดยอัตโนมัติ สิ่งนี้ทำให้ผู้พัฒนาไม่ต้องเขียนชนิดที่แน่นอนซึ่งประกอบด้วย Generic wrappers (VStack, Group, ModifiedContent)
ได้ ตั้งแต่ Swift 5.7 some ในพารามิเตอร์คือน้ำตาลเชิงไวยากรณ์บนพารามิเตอร์ generic มันทำให้การประกาศฟังก์ชันง่ายขึ้น โดยเฉพาะเมื่อทำงานกับ protocol ซึ่งแต่ละ some-parameter ไม่ต้องมี
คอมไพเลอร์จะให้ข้อผิดพลาด: opaque type ต้องการให้ทุกสาขา return ส่งกลับชนิดที่แท้จริงเดียวกัน นี่ทำขึ้นโดยเจตนาเพื่อรักษาเอกลักษณ์ของชนิด หากต้องส่งกลับชนิดต่างกัน ให้ใช้ any
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ