“ ข้อผิดพลาดร้ายแรง: พบศูนย์โดยไม่คาดคิดขณะแกะค่าทางเลือก” หมายความว่าอย่างไร
โปรแกรม Swift ของฉันขัดข้องEXC_BAD_INSTRUCTION
และเกิดข้อผิดพลาดที่คล้ายกันอย่างใดอย่างหนึ่งต่อไปนี้ ข้อผิดพลาดนี้หมายความว่าอย่างไรและฉันจะแก้ไขได้อย่างไร
ข้อผิดพลาดร้ายแรง: พบศูนย์โดยไม่คาดคิดขณะคลายค่าตัวเลือก
หรือ
ข้อผิดพลาดร้ายแรง: ไม่พบศูนย์โดยไม่คาดคิดขณะแกะค่าทางเลือกโดยปริยาย
โพสต์นี้มีวัตถุประสงค์เพื่อรวบรวมคำตอบสำหรับปัญหาที่ "ไม่พบโดยไม่คาดคิด" เพื่อไม่ให้กระจัดกระจายและหายาก อย่าลังเลที่จะเพิ่มคำตอบของคุณเองหรือแก้ไขคำตอบวิกิที่มีอยู่
คำตอบ
คำตอบนี้เป็นวิกิพีเดียชุมชน หากคุณคิดว่าสามารถปรับปรุงให้ดีขึ้นได้อย่าลังเลที่จะแก้ไข
ความเป็นมา: อะไรเป็นทางเลือก?
ใน Swift Optional<Wrapped>
เป็นประเภทตัวเลือก : สามารถมีค่าใดก็ได้จากประเภท ("Wrapped") ดั้งเดิมหรือไม่มีค่าเลย (ค่าพิเศษnil
) ต้องคลายค่าที่เป็นทางเลือกก่อนจึงจะใช้งานได้
ทางเลือกคือประเภททั่วไปซึ่งหมายความว่าOptional<Int>
และOptional<String>
เป็นประเภทที่แตกต่างกันประเภทที่อยู่ภายใน<>
เรียกว่าประเภทห่อ ภายใต้ประทุนตัวเลือกคือenum ที่มีสองกรณี: .some(Wrapped)
และ.none
ที่.none
เทียบเท่ากับnil
.
สามารถประกาศตัวเลือกโดยใช้ประเภทที่ตั้งชื่อOptional<T>
หรือ (โดยทั่วไป) เป็นชวเลขที่มี?
คำต่อท้าย
var anInt: Int = 42
var anOptionalInt: Int? = 42
var anotherOptionalInt: Int? // `nil` is the default when no value is provided
var aVerboseOptionalInt: Optional<Int> // equivalent to `Int?`
anOptionalInt = nil // now this variable contains nil instead of an integer
Optionals เป็นเครื่องมือที่เรียบง่าย แต่ทรงพลังในการแสดงสมมติฐานของคุณขณะเขียนโค้ด คอมไพเลอร์สามารถใช้ข้อมูลนี้เพื่อป้องกันไม่ให้คุณทำผิดพลาด จากภาษาโปรแกรม Swift :
Swift เป็นภาษาที่ปลอดภัยสำหรับประเภทซึ่งหมายความว่าภาษานี้ช่วยให้คุณมีความชัดเจนเกี่ยวกับประเภทของค่าที่โค้ดของคุณสามารถใช้ได้ หากส่วนหนึ่งของรหัสของคุณต้องใช้
String
ประเภทความปลอดภัยจะป้องกันไม่ให้คุณส่งผ่านInt
โดยไม่ได้ตั้งใจ ในทำนองเดียวกันประเภทป้องกันความปลอดภัยให้คุณเผลอผ่านตัวชิ้นส่วนของรหัสที่ต้องไม่เลือกString
String
ความปลอดภัยของประเภทช่วยให้คุณตรวจจับและแก้ไขข้อผิดพลาดได้เร็วที่สุดในกระบวนการพัฒนา
ภาษาโปรแกรมอื่น ๆ บางภาษายังมีประเภทตัวเลือกทั่วไปเช่นอาจจะใน Haskell ตัวเลือกใน Rust และตัวเลือกใน C ++ 17
ในภาษาโปรแกรมที่ไม่มีประเภทตัวเลือกมักใช้ค่า "sentinel"เพื่อบ่งชี้ว่าไม่มีค่าที่ถูกต้อง ตัวอย่างเช่นใน Objective-C nil
( ตัวชี้ null ) แสดงถึงการขาดวัตถุ สำหรับประเภทดั้งเดิมเช่นint
ไม่สามารถใช้ตัวชี้ค่าว่างได้ดังนั้นคุณจะต้องมีตัวแปรแยกต่างหาก (เช่นvalue: Int
และisValid: Bool
) หรือค่า Sentinel ที่กำหนด (เช่น-1
หรือINT_MIN
) วิธีการเหล่านี้เกิดข้อผิดพลาดได้ง่ายเพราะง่ายต่อการลืมตรวจสอบisValid
หรือตรวจสอบค่า Sentinel นอกจากนี้หากมีการเลือกค่าใดค่าหนึ่งเป็น Sentinel นั่นหมายความว่าจะไม่สามารถถือว่าเป็นค่าที่ถูกต้องได้อีกต่อไป
ประเภทตัวเลือกเช่น Swift จะOptional
แก้ปัญหาเหล่านี้โดยการแนะนำค่าพิเศษแยกต่างหากnil
(ดังนั้นคุณไม่จำเป็นต้องกำหนดค่า Sentinel) และโดยการใช้ประโยชน์จากระบบประเภทที่แข็งแกร่งเพื่อให้คอมไพเลอร์สามารถช่วยคุณอย่าลืมตรวจสอบศูนย์เมื่อจำเป็น
เหตุใดฉันจึงได้รับ " ข้อผิดพลาดร้ายแรง: พบศูนย์โดยไม่คาดคิดขณะคลายค่าตัวเลือก "
ในการเข้าถึงค่าของทางเลือก (ถ้ามีเลย) คุณต้องแกะออก ค่าที่เป็นทางเลือกสามารถคลายออกได้อย่างปลอดภัยหรือบังคับ หากคุณบังคับให้แกะตัวเลือกเสริมและไม่มีค่าโปรแกรมของคุณจะขัดข้องด้วยข้อความด้านบน
Xcode จะแสดงข้อขัดข้องโดยการเน้นบรรทัดของโค้ด ปัญหาเกิดขึ้นในบรรทัดนี้
ข้อขัดข้องนี้อาจเกิดขึ้นได้กับการแกะแรงสองประเภท:
1. กำลังยกเลิกการห่ออย่างชัดเจน
สิ่งนี้ทำได้โดยใช้!
ตัวดำเนินการบนตัวเลือก ตัวอย่างเช่น:
let anOptionalString: String?
print(anOptionalString!) // <- CRASH
ข้อผิดพลาดร้ายแรง: พบศูนย์โดยไม่คาดคิดขณะคลายค่าตัวเลือก
ในฐานะที่anOptionalString
เป็นnil
ที่นี่คุณจะได้รับความผิดพลาดในบรรทัดที่คุณบังคับให้แกะมัน
2. ตัวเลือกที่ไม่ได้ห่อโดยปริยาย
สิ่งเหล่านี้ถูกกำหนดด้วย a !
แทนที่จะเป็น?
หลังประเภท
var optionalDouble: Double! // this value is implicitly unwrapped wherever it's used
ตัวเลือกเหล่านี้ถือว่ามีค่า ดังนั้นเมื่อใดก็ตามที่คุณเข้าถึงอุปกรณ์เสริมที่ไม่ได้ปิดกั้นโดยปริยายระบบจะบังคับให้คุณปลดล็อกโดยอัตโนมัติ หากไม่มีค่าก็จะหยุดทำงาน
print(optionalDouble) // <- CRASH
ข้อผิดพลาดร้ายแรง: ไม่พบศูนย์โดยไม่คาดคิดขณะแกะค่าทางเลือกโดยปริยาย
หากต้องการทราบว่าตัวแปรใดที่ทำให้เกิดข้อขัดข้องคุณสามารถกดค้างไว้⌥ในขณะที่คลิกเพื่อแสดงคำจำกัดความซึ่งคุณอาจพบประเภทที่ไม่บังคับ
โดยเฉพาะอย่างยิ่ง IBOutlets มักเป็นตัวเลือกที่ไม่มีการปิดกั้นโดยปริยาย เนื่องจาก xib หรือสตอรีบอร์ดของคุณจะเชื่อมโยงร้านค้าที่รันไทม์หลังจากเริ่มต้น ดังนั้นคุณควรตรวจสอบให้แน่ใจว่าคุณไม่ได้เข้าถึงร้านค้าก่อนที่จะโหลดเข้ามาคุณควรตรวจสอบด้วยว่าการเชื่อมต่อถูกต้องในไฟล์สตอรี่บอร์ด / xib ของคุณมิฉะนั้นค่าจะอยู่nil
ที่รันไทม์ดังนั้นจึงเกิดข้อผิดพลาดเมื่อพวกเขาไม่ได้ติดตั้งโดยปริยาย . เมื่อแก้ไขการเชื่อมต่อให้ลองลบบรรทัดของรหัสที่กำหนดร้านค้าของคุณจากนั้นเชื่อมต่อใหม่
เมื่อใดที่ฉันควรบังคับแกะอุปกรณ์เสริม
กำลังยกเลิกการห่ออย่างชัดเจน
ตามกฎทั่วไปคุณไม่ควรบังคับให้แกะอุปกรณ์เสริมกับตัว!
ดำเนินการอย่างชัดเจน อาจมีบางกรณีที่!
ยอมรับการใช้งานได้ - แต่คุณควรใช้มันก็ต่อเมื่อคุณแน่ใจ 100% ว่าตัวเลือกนั้นมีค่า
ในขณะที่อาจมีบางครั้งที่คุณสามารถใช้การบังคับแกะได้อย่างที่คุณทราบเนื่องจากข้อเท็จจริงที่ว่าตัวเลือกนั้นมีค่า แต่ไม่มีที่เดียวที่คุณไม่สามารถแกะตัวเลือกนั้นออกมาได้อย่างปลอดภัยแทน
ตัวเลือกที่ไม่ได้ห่อโดยปริยาย
ตัวแปรเหล่านี้ได้รับการออกแบบมาเพื่อให้คุณสามารถเลื่อนการมอบหมายออกไปได้จนกว่าจะถึงรหัสของคุณในภายหลัง เป็นความรับผิดชอบของคุณที่จะต้องตรวจสอบให้แน่ใจว่าพวกเขามีค่าก่อนที่คุณจะเข้าถึง อย่างไรก็ตามเนื่องจากพวกเขาเกี่ยวข้องกับการบังคับให้แกะออกจึงยังคงไม่ปลอดภัยโดยเนื้อแท้เนื่องจากถือว่าค่าของคุณไม่ใช่ศูนย์แม้ว่าการกำหนดค่าศูนย์จะถูกต้อง
คุณควรใช้ตัวเลือกที่ไม่ได้ปิดโดยปริยายเป็นทางเลือกสุดท้ายเท่านั้น หากคุณสามารถใช้ตัวแปร lazyหรือระบุค่าเริ่มต้นสำหรับตัวแปร - คุณควรทำเช่นนั้นแทนที่จะใช้ตัวเลือกที่ไม่ได้ใส่ไว้โดยปริยาย
อย่างไรก็ตามมีบางสถานการณ์ที่ยังไม่ได้เปิดโดยปริยาย optionals เป็นประโยชน์และคุณจะยังคงสามารถที่จะใช้วิธีการต่างๆอย่างปลอดภัย unwrapping พวกเขาตามที่ระบุไว้ดังต่อไปนี้ - แต่คุณควรเสมอใช้พวกเขาด้วยความระมัดระวังตามสมควร
ฉันจะจัดการกับ Optionals อย่างปลอดภัยได้อย่างไร?
nil
วิธีที่ง่ายที่สุดเพื่อตรวจสอบว่าเป็นตัวเลือกที่มีค่าคือการเปรียบเทียบกับ
if anOptionalInt != nil {
print("Contains a value!")
} else {
print("Doesn’t contain a value.")
}
อย่างไรก็ตาม 99.9% ของเวลาที่ทำงานกับ optionals คุณจะต้องการเข้าถึงค่าที่มีอยู่หากมีอยู่เลย ในการดำเนินการนี้คุณสามารถใช้การผูกเพิ่มเติมได้
การผูกเพิ่มเติม
การผูกเสริมช่วยให้คุณตรวจสอบได้ว่าตัวเลือกมีค่าหรือไม่และอนุญาตให้คุณกำหนดค่าที่ไม่ได้ปิดกั้นให้กับตัวแปรหรือค่าคงที่ใหม่ มันใช้ไวยากรณ์if let x = anOptional {...}
หรือif var x = anOptional {...}
ขึ้นอยู่กับว่าคุณต้องการแก้ไขค่าของตัวแปรใหม่หลังจากผูกมันแล้ว
ตัวอย่างเช่น:
if let number = anOptionalInt {
print("Contains a value! It is \(number)!")
} else {
print("Doesn’t contain a number")
}
สิ่งนี้ทำก่อนตรวจสอบว่าทางเลือกมีค่า ถ้ามันไม่ได้แล้ว 'ยังไม่ได้เปิด' ค่าถูกกำหนดให้กับตัวแปรใหม่ ( number
) - ซึ่งคุณสามารถใช้อย่างอิสระราวกับว่ามันก็ไม่ใช่ตัวเลือก หากตัวเลือกไม่มีค่าคำสั่งอื่นจะถูกเรียกใช้ตามที่คุณคาดหวัง
สิ่งที่เป็นระเบียบเกี่ยวกับการผูกที่เป็นทางเลือกคือคุณสามารถแกะตัวเลือกหลายตัวพร้อมกันได้ คุณสามารถแยกข้อความโดยใช้ลูกน้ำ คำสั่งจะประสบความสำเร็จหากตัวเลือกทั้งหมดถูกแกะออก
var anOptionalInt : Int?
var anOptionalString : String?
if let number = anOptionalInt, let text = anOptionalString {
print("anOptionalInt contains a value: \(number). And so does anOptionalString, it’s: \(text)")
} else {
print("One or more of the optionals don’t contain a value")
}
เคล็ดลับอีกอย่างหนึ่งคือคุณสามารถใช้เครื่องหมายจุลภาคเพื่อตรวจสอบเงื่อนไขบางอย่างของค่าหลังจากแกะออกได้
if let number = anOptionalInt, number > 0 {
print("anOptionalInt contains a value: \(number), and it’s greater than zero!")
}
สิ่งเดียวที่จับได้ด้วยการใช้การผูกที่เป็นทางเลือกภายในคำสั่ง if คือคุณสามารถเข้าถึงเฉพาะค่าที่ไม่ได้ปิดกั้นจากภายในขอบเขตของคำสั่งเท่านั้น หากคุณจำเป็นต้องเข้าถึงค่าจากนอกขอบเขตของคำสั่งที่คุณสามารถใช้คำสั่งยาม
งบยามช่วยให้คุณกำหนดเงื่อนไขในการประสบความสำเร็จ - และขอบเขตปัจจุบันจะยังคงดำเนินการถ้าเงื่อนไขที่จะพบ guard condition else {...}
พวกเขาจะถูกกำหนดด้วยไวยากรณ์
ดังนั้นหากต้องการใช้กับการผูกเพิ่มเติมคุณสามารถทำได้:
guard let number = anOptionalInt else {
return
}
(โปรดทราบว่าภายในหน่วยรักษาความปลอดภัยคุณต้องใช้หนึ่งในคำสั่งการถ่ายโอนการควบคุมเพื่อที่จะออกจากขอบเขตของรหัสที่ดำเนินการอยู่ในปัจจุบัน)
หากanOptionalInt
มีค่าก็จะถูกคลายออกและกำหนดให้กับnumber
ค่าคงที่ใหม่ รหัสหลังยามจะดำเนินการต่อไป หากไม่มีค่า - ตัวป้องกันจะรันโค้ดภายในวงเล็บซึ่งจะนำไปสู่การถ่ายโอนการควบคุมดังนั้นโค้ดหลังจากนั้นจะไม่ถูกเรียกใช้งาน
สิ่งที่เรียบร้อยจริงเกี่ยวกับงบยามเป็นค่าที่ยังไม่ได้เปิดอยู่ในขณะนี้ที่จะใช้ในรหัสที่เป็นไปตามคำสั่ง (ที่เรารู้ว่ารหัสในอนาคตที่สามารถเพียงรันถ้าตัวเลือกมีค่า) นี่เป็นวิธีที่ยอดเยี่ยมสำหรับการกำจัด'pyramids of doom' ที่สร้างโดยการซ้อนคำสั่ง if หลาย ๆ
ตัวอย่างเช่น:
guard let number = anOptionalInt else {
return
}
print("anOptionalInt contains a value, and it’s: \(number)!")
ยามยังสนับสนุนกลเม็ดแบบเดียวกับที่คำสั่ง if รองรับเช่นการคลายตัวเลือกหลายตัวพร้อมกันและใช้where
ประโยคคำสั่ง
ไม่ว่าคุณจะใช้คำสั่ง if หรือ guard โดยสมบูรณ์ขึ้นอยู่กับว่ารหัสในอนาคตต้องการให้ตัวเลือกมีค่าหรือไม่
Nil Coalescing Operator
Nil หลอมรวมผู้ประกอบการเป็นรุ่นที่จดชวเลขที่ดีของผู้ประกอบการที่มีเงื่อนไขประกอบไปด้วยการออกแบบเป็นหลักในการแปลง optionals ไม่ใช่ optionals- มีไวยากรณ์a ?? b
ซึ่งa
เป็นประเภททางเลือกและb
เป็นประเภทเดียวกับa
(แม้ว่าโดยปกติจะไม่ใช่ทางเลือกก็ตาม)
โดยพื้นฐานแล้วให้คุณพูดว่า "ถ้าa
มีค่าให้แกะมันออก ถ้าไม่เป็นเช่นนั้นให้กลับมาb
แทน”. ตัวอย่างเช่นคุณสามารถใช้งานได้ดังนี้:
let number = anOptionalInt ?? 0
สิ่งนี้จะกำหนดnumber
ค่าคงที่ของInt
ประเภทซึ่งจะมีค่าของanOptionalInt
หากมีค่าหรือ0
อย่างอื่น
เป็นเพียงชวเลขสำหรับ:
let number = anOptionalInt != nil ? anOptionalInt! : 0
โซ่เสริม
คุณสามารถใช้Optional Chainingเพื่อเรียกเมธอดหรือเข้าถึงคุณสมบัติบนทางเลือก ทำได้ง่ายๆโดยการต่อท้ายชื่อตัวแปรด้วย a ?
เมื่อใช้งาน
ตัวอย่างเช่นสมมติว่าเรามีตัวแปรfoo
ประเภทFoo
อินสแตนซ์เสริม
var foo : Foo?
หากเราต้องการเรียกวิธีการfoo
ที่ไม่ส่งคืนอะไรเลยเราสามารถทำได้:
foo?.doSomethingInteresting()
หากfoo
มีค่าวิธีนี้จะถูกเรียกใช้ หากไม่เป็นเช่นนั้นจะไม่มีอะไรเลวร้ายเกิดขึ้น - โค้ดจะดำเนินการต่อไป
(ลักษณะนี้คล้ายกับการส่งข้อความไปยังnil
Objective-C)
ดังนั้นจึงสามารถใช้ในการตั้งค่าคุณสมบัติเช่นเดียวกับวิธีการโทร ตัวอย่างเช่น:
foo?.bar = Bar()
อีกครั้งไม่มีอะไรเลวร้ายที่จะเกิดขึ้นที่นี่ถ้าเป็นfoo
nil
รหัสของคุณจะดำเนินการต่อไป
เคล็ดลับอีกอย่างหนึ่งที่การผูกมัดทางเลือกช่วยให้คุณทำได้คือตรวจสอบว่าการตั้งค่าคุณสมบัติหรือการเรียกวิธีสำเร็จ nil
คุณสามารถทำได้โดยการเปรียบเทียบค่าที่ส่งกลับไป
(เนื่องจากค่าที่เป็นทางเลือกจะส่งคืนVoid?
แทนที่จะVoid
เป็นวิธีการที่ไม่ส่งคืนอะไรเลย)
ตัวอย่างเช่น:
if (foo?.bar = Bar()) != nil {
print("bar was set successfully")
} else {
print("bar wasn’t set successfully")
}
อย่างไรก็ตามสิ่งต่างๆจะยุ่งยากขึ้นเล็กน้อยเมื่อพยายามเข้าถึงคุณสมบัติหรือเรียกเมธอดที่ส่งคืนค่า เนื่องจากfoo
เป็นทางเลือกสิ่งที่ส่งคืนจากสิ่งนั้นจะเป็นทางเลือก ในการจัดการกับสิ่งนี้คุณสามารถแกะตัวเลือกที่ได้รับกลับมาโดยใช้วิธีใดวิธีหนึ่งข้างต้นหรือแกะfoo
ตัวเองก่อนที่จะเข้าถึงเมธอดหรือวิธีการเรียกใช้ที่ส่งคืนค่า
นอกจากนี้ตามชื่อที่แนะนำคุณสามารถ "เชื่อมโยง" ข้อความเหล่านี้เข้าด้วยกันได้ ซึ่งหมายความว่าถ้าfoo
มีคุณสมบัติทางเลือกbaz
ซึ่งมีคุณสมบัติqux
- คุณสามารถเขียนสิ่งต่อไปนี้:
let optionalQux = foo?.baz?.qux
อีกครั้งเนื่องจากfoo
และbaz
เป็นทางเลือกค่าที่ส่งคืนจากqux
จะเป็นทางเลือกเสมอไม่ว่าจะqux
เป็นทางเลือกหรือไม่ก็ตาม
map
และ flatMap
คุณลักษณะที่มักใช้ไม่ได้กับตัวเลือกคือความสามารถในการใช้ฟังก์ชันmap
และ flatMap
สิ่งเหล่านี้ช่วยให้คุณสามารถใช้การแปลงที่ไม่ใช่ทางเลือกกับตัวแปรที่เป็นทางเลือกได้ หากตัวเลือกมีค่าคุณสามารถใช้การแปลงที่กำหนดกับค่านั้นได้ nil
ถ้ามันไม่ได้มีค่าก็จะยังคงอยู่
ตัวอย่างเช่นสมมติว่าคุณมีสตริงที่เป็นทางเลือก:
let anOptionalString:String?
ด้วยการใช้map
ฟังก์ชันนี้ - เราสามารถใช้stringByAppendingString
ฟังก์ชันนี้เพื่อเชื่อมต่อกับสตริงอื่น
เนื่องจากstringByAppendingString
ใช้อาร์กิวเมนต์สตริงที่ไม่เป็นทางเลือกเราจึงไม่สามารถป้อนสตริงที่เป็นทางเลือกได้โดยตรง อย่างไรก็ตามโดยการใช้map
เราสามารถใช้ allow stringByAppendingString
to be used if anOptionalString
have a value.
ตัวอย่างเช่น:
var anOptionalString:String? = "bar"
anOptionalString = anOptionalString.map {unwrappedString in
return "foo".stringByAppendingString(unwrappedString)
}
print(anOptionalString) // Optional("foobar")
แต่ถ้าanOptionalString
ไม่ได้มีค่าจะกลับมาmap
nil
ตัวอย่างเช่น:
var anOptionalString:String?
anOptionalString = anOptionalString.map {unwrappedString in
return "foo".stringByAppendingString(unwrappedString)
}
print(anOptionalString) // nil
flatMap
ใช้งานได้ในทำนองเดียวกันmap
ยกเว้นจะอนุญาตให้คุณส่งคืนตัวเลือกอื่นจากภายในตัวปิด ซึ่งหมายความว่าคุณสามารถป้อนข้อมูลทางเลือกในกระบวนการที่ต้องใช้อินพุตที่ไม่ใช่ทางเลือก แต่สามารถส่งออกเป็นทางเลือกได้เอง
try!
ระบบจัดการข้อผิดพลาดของ Swift สามารถใช้ได้อย่างปลอดภัยกับDo-Try-Catch :
do {
let result = try someThrowingFunc()
} catch {
print(error)
}
หากsomeThrowingFunc()
เกิดข้อผิดพลาดข้อผิดพลาดจะถูกจับได้อย่างปลอดภัยในcatch
บล็อก
error
คงที่คุณเห็นในcatch
บล็อกยังไม่ได้รับการประกาศโดยเรา - catch
มันสร้างขึ้นโดยอัตโนมัติ
นอกจากนี้คุณยังสามารถประกาศerror
ตัวเองได้ซึ่งมีข้อดีคือสามารถแคสต์เป็นรูปแบบที่มีประโยชน์เช่น:
do {
let result = try someThrowingFunc()
} catch let error as NSError {
print(error.debugDescription)
}
การใช้try
วิธีนี้เป็นวิธีที่เหมาะสมในการพยายามจับและจัดการข้อผิดพลาดที่มาจากฟังก์ชันการขว้างปา
นอกจากนี้ยังมีการtry?
ดูดซับข้อผิดพลาด:
if let result = try? someThrowingFunc() {
// cool
} else {
// handle the failure, but there's no error information available
}
แต่ระบบจัดการข้อผิดพลาดของ Swift ยังให้วิธี "บังคับลอง" ด้วยtry!
:
let result = try! someThrowingFunc()
แนวคิดที่อธิบายไว้ในโพสต์นี้ยังนำไปใช้ที่นี่ด้วย: หากเกิดข้อผิดพลาดแอปพลิเคชันจะหยุดทำงาน
คุณควรใช้ก็ต่อtry!
เมื่อคุณสามารถพิสูจน์ได้ว่าผลลัพธ์ของมันจะไม่ล้มเหลวในบริบทของคุณ - ซึ่งหายากมาก
เวลาส่วนใหญ่คุณจะใช้ระบบ Do-Try-Catch ที่สมบูรณ์ - และระบบที่เป็นทางเลือกtry?
ในบางกรณีที่การจัดการข้อผิดพลาดไม่สำคัญ
ทรัพยากร
TL; DR คำตอบ
มีข้อยกเว้นน้อยมากกฎนี้จึงเป็นสีทอง:
หลีกเลี่ยงการใช้ !
ประกาศตัวแปรทางเลือก ( ?
) ไม่ใช่ตัวเลือกที่ไม่ได้ปิดโดยปริยาย (IUO) ( !
)
กล่าวอีกนัยหนึ่งแทนที่จะใช้:
var nameOfDaughter: String?
แทน:
var nameOfDaughter: String!
แกะตัวแปรเสริมโดยใช้if let
หรือguard let
แกะตัวแปรเช่นนี้:
if let nameOfDaughter = nameOfDaughter {
print("My daughters name is: \(nameOfDaughter)")
}
หรือเช่นนี้:
guard let nameOfDaughter = nameOfDaughter else { return }
print("My daughters name is: \(nameOfDaughter)")
คำตอบนี้มีวัตถุประสงค์เพื่อให้กระชับเพื่อความเข้าใจทั้งหมดที่ได้รับการยอมรับในการอ่านคำตอบ
ทรัพยากร
คำถามนี้เกิดขึ้นตลอดเวลาใน SO เป็นหนึ่งในสิ่งแรก ๆ ที่นักพัฒนา Swift ใหม่ต้องเผชิญ
พื้นหลัง:
Swift ใช้แนวคิดของ "Optionals" เพื่อจัดการกับค่าที่อาจมีค่าหรือไม่ก็ได้ ในภาษาอื่นเช่น C คุณอาจเก็บค่า 0 ไว้ในตัวแปรเพื่อระบุว่าไม่มีค่า อย่างไรก็ตามถ้า 0 เป็นค่าที่ถูกต้องล่ะ? จากนั้นคุณอาจใช้ -1 เกิดอะไรขึ้นถ้า -1 เป็นค่าที่ถูกต้อง? และอื่น ๆ
ตัวเลือก Swift ช่วยให้คุณตั้งค่าตัวแปรประเภทใดก็ได้เพื่อให้มีค่าที่ถูกต้องหรือไม่มีค่า
คุณใส่เครื่องหมายคำถามหลังประเภทเมื่อคุณประกาศตัวแปรเป็นค่าเฉลี่ย (พิมพ์ x หรือไม่มีค่า)
ทางเลือกเป็นคอนเทนเนอร์ที่มีตัวแปรประเภทที่กำหนดหรือไม่มีอะไรเลย
ตัวเลือกจำเป็นต้อง "คลายออก" เพื่อดึงค่าภายใน
การ "!" ตัวดำเนินการคือตัวดำเนินการ "บังคับแกะ" มันบอกว่า "เชื่อฉันฉันรู้ว่าฉันกำลังทำอะไรฉันรับประกันว่าเมื่อโค้ดนี้ทำงานตัวแปรจะไม่มีศูนย์" ถ้าคุณผิดคุณผิดพลาด
เว้นแต่คุณจะไม่รู้จริงๆว่ากำลังทำอะไรอยู่ให้หลีกเลี่ยง "!" บังคับตัวดำเนินการแกะ อาจเป็นแหล่งที่มาของข้อขัดข้องที่ใหญ่ที่สุดสำหรับการเริ่มต้นโปรแกรมเมอร์ Swift
วิธีจัดการกับ optionals:
มีวิธีอื่น ๆ อีกมากมายในการจัดการกับตัวเลือกที่ปลอดภัยกว่า นี่คือบางส่วน (ไม่ใช่รายการทั้งหมด)
คุณสามารถใช้ "การผูกที่เป็นทางเลือก" หรือ "if let" เพื่อบอกว่า "ถ้าตัวเลือกนี้มีค่าอยู่ให้บันทึกค่านั้นลงในตัวแปรใหม่ที่ไม่ใช่ทางเลือกหากตัวเลือกนั้นไม่มีค่าให้ข้ามเนื้อหาของคำสั่ง if นี้ ".
นี่คือตัวอย่างของการผูกแบบเลือกได้กับfoo
ทางเลือกของเรา:
if let newFoo = foo //If let is called optional binding. {
print("foo is not nil")
} else {
print("foo is nil")
}
โปรดสังเกตว่าตัวแปรที่คุณกำหนดเมื่อคุณใช้การเสนอราคาที่เป็นทางเลือกนั้นมีอยู่ (เฉพาะ "ในขอบเขต") ในเนื้อความของคำสั่ง if
อีกวิธีหนึ่งคุณสามารถใช้คำสั่งยามซึ่งช่วยให้คุณออกจากฟังก์ชันของคุณหากตัวแปรเป็นศูนย์:
func aFunc(foo: Int?) {
guard let newFoo = input else { return }
//For the rest of the function newFoo is a non-optional var
}
คำสั่ง Guard ถูกเพิ่มใน Swift 2 Guard ช่วยให้คุณสามารถรักษา "เส้นทางสีทอง" ผ่านโค้ดของคุณและหลีกเลี่ยงระดับ ifs ที่ซ้อนกันที่เพิ่มขึ้นเรื่อย ๆ ซึ่งบางครั้งเป็นผลมาจากการใช้การเชื่อมโยงแบบเลือก "if let"
นอกจากนี้ยังมีโครงสร้างที่เรียกว่า "nil coalescing operator" โดยอยู่ในรูปแบบ "optional_var ?? replacement_val" มันส่งคืนตัวแปรที่ไม่ใช่ทางเลือกที่มีประเภทเดียวกับข้อมูลที่มีอยู่ในทางเลือก หากทางเลือกมีศูนย์จะส่งกลับค่าของนิพจน์หลัง "??" สัญลักษณ์.
ดังนั้นคุณสามารถใช้รหัสดังนี้:
let newFoo = foo ?? "nil" // "??" is the nil coalescing operator
print("foo = \(newFoo)")
คุณยังสามารถใช้การจัดการข้อผิดพลาด try / catch หรือ guard ได้ แต่โดยทั่วไปแล้วหนึ่งในเทคนิคอื่น ๆ ข้างต้นจะสะอาดกว่า
แก้ไข:
gotcha ที่ละเอียดกว่าเล็กน้อยพร้อมตัวเลือกคือ "ตัวเลือกที่ไม่ได้ห่อโดยปริยายเมื่อเราประกาศ foo เราสามารถพูดว่า:
var foo: String!
ในกรณีนี้ foo ยังคงเป็นทางเลือก แต่คุณไม่จำเป็นต้องแกะเพื่ออ้างอิง นั่นหมายความว่าเมื่อใดก็ตามที่คุณพยายามอ้างอิง foo คุณจะผิดพลาดหากเป็นศูนย์
ดังนั้นรหัสนี้:
var foo: String!
let upperFoo = foo.capitalizedString
จะผิดพลาดในการอ้างอิงถึงคุณสมบัติตัวพิมพ์ใหญ่ของ foo แม้ว่าเราจะไม่บังคับให้คลายตัว foo ก็ตาม งานพิมพ์ดูดี แต่ไม่ใช่
ดังนั้นคุณต้องระมัดระวังอย่างมากกับตัวเลือกที่ไม่ได้ปิดโดยปริยาย (และอาจหลีกเลี่ยงได้อย่างสมบูรณ์จนกว่าคุณจะมีความเข้าใจที่มั่นคงเกี่ยวกับตัวเลือกต่างๆ)
บรรทัดด้านล่าง: เมื่อคุณเรียน Swift เป็นครั้งแรกให้แสร้งทำเป็น "!" อักขระไม่ได้เป็นส่วนหนึ่งของภาษา มีแนวโน้มที่จะทำให้คุณมีปัญหา
เนื่องจากคำตอบข้างต้นอธิบายวิธีการเล่นอย่างปลอดภัยด้วย Optionals ฉันจะพยายามอธิบายว่าตัวเลือกใดบ้างที่รวดเร็ว
อีกวิธีหนึ่งในการประกาศตัวแปรทางเลือกคือ
var i : Optional<Int>
และประเภททางเลือกคืออะไรนอกจากการแจงนับโดยมีสองกรณีคือ
enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
.
.
.
}
ดังนั้นเพื่อกำหนดศูนย์ให้กับตัวแปร 'i' ของเรา เราสามารถทำ
var i = Optional<Int>.none
หรือกำหนดค่าเราจะส่งผ่านค่าบางอย่าง
var i = Optional<Int>.some(28)
ตามความรวดเร็ว 'nil' คือการไม่มีค่า และในการสร้างอินสแตนซ์ที่เริ่มต้นด้วยnil
เราต้องเป็นไปตามโปรโตคอลที่เรียกว่าExpressibleByNilLiteral
และดีมากถ้าคุณเดาได้เราไม่แนะนำให้Optionals
ปฏิบัติตามExpressibleByNilLiteral
และสอดคล้องกับประเภทอื่นเท่านั้น
ExpressibleByNilLiteral
มีวิธีการเดียวที่เรียกว่าinit(nilLiteral:)
เริ่มต้นอินสแตนซ์ด้วยศูนย์ โดยปกติคุณจะไม่เรียกเมธอดนี้และตามเอกสารที่รวดเร็วขอแนะนำให้เรียกตัวเริ่มต้นนี้โดยตรงในขณะที่คอมไพเลอร์เรียกใช้เมื่อใดก็ตามที่คุณเริ่มต้นประเภทตัวเลือกด้วยnil
ตัวอักษร
แม้ตัวเองจะต้องห่อ (ไม่มีเล่นสำนวนเจตนา) หัวของฉันรอบ optionals: D มีความสุข Swfting ทั้งหมด
ขั้นแรกคุณควรทราบว่าค่าที่เป็นทางเลือกคืออะไร คุณสามารถไปที่The Swift Programming Languageเพื่อดูรายละเอียด
ประการที่สองคุณควรทราบว่าค่าทางเลือกมีสองสถานะ ค่าหนึ่งคือค่าเต็มและอีกค่าหนึ่งเป็นค่าศูนย์ ดังนั้นก่อนที่คุณจะใช้ค่าที่เป็นทางเลือกคุณควรตรวจสอบว่าเป็นสถานะใด
คุณสามารถใช้if let ...
หรือguard let ... else
อื่น ๆ
อีกวิธีหนึ่งหากคุณไม่ต้องการตรวจสอบสถานะตัวแปรก่อนการใช้งานคุณสามารถใช้var buildingName = buildingName ?? "buildingName"
แทนได้
ฉันมีข้อผิดพลาดนี้ครั้งหนึ่งเมื่อฉันพยายามตั้งค่า Outlets ของฉันจากวิธีการเตรียมการทำต่อดังนี้:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? DestinationVC{
if let item = sender as? DataItem{
// This line pops up the error
destination.nameLabel.text = item.name
}
}
}
จากนั้นฉันพบว่าฉันไม่สามารถตั้งค่าของเต้าเสียบคอนโทรลเลอร์ปลายทางได้เนื่องจากตัวควบคุมยังไม่ได้โหลดหรือเริ่มต้น
ดังนั้นฉันจึงแก้ไขด้วยวิธีนี้:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? DestinationVC{
if let item = sender as? DataItem{
// Created this method in the destination Controller to update its outlets after it's being initialized and loaded
destination.updateView(itemData: item)
}
}
}
ตัวควบคุมปลายทาง:
// This variable to hold the data received to update the Label text after the VIEW DID LOAD
var name = ""
// Outlets
@IBOutlet weak var nameLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
nameLabel.text = name
}
func updateView(itemDate: ObjectModel) {
name = itemDate.name
}
ฉันหวังว่าคำตอบนี้จะช่วยให้ทุกคนที่มีปัญหาเดียวกันกับที่ฉันพบว่าคำตอบที่ทำเครื่องหมายไว้เป็นแหล่งข้อมูลที่ดีในการทำความเข้าใจตัวเลือกและวิธีการทำงาน แต่ยังไม่ได้แก้ไขปัญหาโดยตรง
โดยทั่วไปคุณพยายามใช้ค่าศูนย์ในสถานที่ที่ Swift อนุญาตเฉพาะค่าที่ไม่ใช่ศูนย์โดยบอกให้คอมไพเลอร์เชื่อใจคุณว่าจะไม่มีค่าศูนย์อยู่ที่นั่นดังนั้นจึงอนุญาตให้แอปของคุณรวบรวมได้
มีหลายสถานการณ์ที่นำไปสู่ข้อผิดพลาดร้ายแรงประเภทนี้:
บังคับแกะ:
let user = someVariable!
ถ้า
someVariable
เป็นศูนย์คุณจะได้รับความผิดพลาด การแกะแรงจะทำให้คุณได้ย้ายความรับผิดชอบในการตรวจสอบศูนย์จากคอมไพเลอร์มาให้คุณโดยพื้นฐานแล้วการแกะแบบบังคับคุณรับประกันกับคอมไพเลอร์ว่าคุณจะไม่มีค่าศูนย์ที่นั่น และเดาว่าจะเกิดอะไรขึ้นถ้าค่าศูนย์จบลงด้วยsomeVariable
?วิธีการแก้? ใช้การผูกเพิ่มเติม (aka if-let) ทำการประมวลผลตัวแปรที่นั่น:
if user = someVariable { // do your stuff }
บังคับ (ลง) ร่าย:
let myRectangle = someShape as! Rectangle
การบังคับแคสต์จะเป็นการบอกให้คอมไพเลอร์ไม่ต้องกังวลอีกต่อไปเพราะคุณจะมี
Rectangle
อินสแตนซ์อยู่ที่นั่นเสมอ และตราบใดที่ถือได้ว่าคุณไม่ต้องกังวล ปัญหาเริ่มต้นเมื่อคุณหรือเพื่อนร่วมงานของคุณจากโครงการเริ่มหมุนเวียนค่าที่ไม่ใช่สี่เหลี่ยมผืนผ้าวิธีการแก้? ใช้การผูกเพิ่มเติม (aka if-let) ทำการประมวลผลตัวแปรที่นั่น:
if let myRectangle = someShape as? Rectangle { // yay, I have a rectangle }
ตัวเลือกที่ไม่ได้ปิดโดยปริยาย สมมติว่าคุณมีนิยามคลาสดังต่อไปนี้:
class User { var name: String! init() { name = "(unnamed)" } func nicerName() { return "Mr/Ms " + name } }
ตอนนี้หากไม่มีใครยุ่งกับ
name
คุณสมบัติโดยการตั้งค่าเป็นมันnil
ก็จะทำงานได้ตามที่คาดไว้อย่างไรก็ตามหากUser
เริ่มต้นจาก JSON ที่ไม่มีname
คีย์คุณจะได้รับข้อผิดพลาดร้ายแรงเมื่อพยายามใช้คุณสมบัติวิธีการแก้? อย่าใช้มัน :) เว้นแต่คุณจะแน่ใจ 102% ว่าอสังหาริมทรัพย์นั้นจะมีค่าที่ไม่ใช่ศูนย์เสมอเมื่อถึงเวลาที่ต้องใช้ ในกรณีส่วนใหญ่การแปลงเป็นทางเลือกหรือไม่ใช่ทางเลือกจะใช้ได้ การทำให้ไม่เป็นทางเลือกจะส่งผลให้คอมไพเลอร์ช่วยคุณด้วยการบอกเส้นทางรหัสที่คุณพลาดโดยให้ค่าแก่คุณสมบัตินั้น
เต้าเสียบที่ไม่ได้เชื่อมต่อหรือยังไม่ได้เชื่อมต่อ นี่เป็นกรณีเฉพาะของสถานการณ์สมมติ # 3 โดยทั่วไปคุณมีคลาสที่โหลด XIB ที่คุณต้องการใช้
class SignInViewController: UIViewController { @IBOutlet var emailTextField: UITextField! }
ตอนนี้หากคุณพลาดการเชื่อมต่อเต้าเสียบจากตัวแก้ไข XIB แอปจะหยุดทำงานทันทีที่คุณต้องการใช้เต้าเสียบ วิธีการแก้? ตรวจสอบให้แน่ใจว่าเสียบปลั๊กไฟทั้งหมดแล้ว หรือใช้ตัว
?
ดำเนินการกับพวกเขา:emailTextField?.text = "[email protected]"
. หรือประกาศว่าเต้าเสียบเป็นทางเลือกแม้ว่าในกรณีนี้คอมไพเลอร์จะบังคับให้คุณแกะมันออกจากโค้ดทั้งหมดค่าที่มาจาก Objective-C และไม่มีคำอธิบายประกอบที่เป็นโมฆะ สมมติว่าเรามีคลาส Objective-C ดังต่อไปนี้:
@interface MyUser: NSObject @property NSString *name; @end
ตอนนี้หากไม่มีการระบุคำอธิบายประกอบที่เป็นโมฆะ (ไม่ว่าจะโดยชัดแจ้งหรือผ่าน
NS_ASSUME_NONNULL_BEGIN
/NS_ASSUME_NONNULL_END
)name
คุณสมบัติจะถูกอิมพอร์ตใน Swift เป็นString!
(IUO - ไม่จำเป็นต้องปิดโดยปริยาย) ทันทีที่โค้ดname
ด่วนบางโค้ดต้องการใช้ค่าโค้ดจะหยุดทำงานหากไม่มีวิธีการแก้? เพิ่มคำอธิบายประกอบที่เป็นโมฆะให้กับรหัส Objective-C ของคุณ ระวังคอมไพเลอร์ Objective-C นั้นอนุญาตเล็กน้อยเมื่อพูดถึงความว่างเปล่าคุณอาจลงเอยด้วยค่าศูนย์แม้ว่าคุณจะทำเครื่องหมายไว้อย่างชัดเจนว่าเป็น
nonnull
.
นี่เป็นความคิดเห็นที่สำคัญมากกว่าและเหตุใดตัวเลือกที่ไม่ได้ปิดโดยปริยายจึงสามารถหลอกลวงได้เมื่อพูดถึงการดีบักnil
ค่า
นึกถึงรหัสต่อไปนี้: รวบรวมโดยไม่มีข้อผิดพลาด / คำเตือน:
c1.address.city = c3.address.city
แต่เมื่อรันไทม์จะให้ข้อผิดพลาดต่อไปนี้: ข้อผิดพลาดร้ายแรง: พบศูนย์โดยไม่คาดคิดขณะคลายค่าที่เป็นทางเลือก
บอกได้ไหมว่าวัตถุชิ้นnil
ไหน
คุณทำไม่ได้!
รหัสเต็มจะเป็น:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var c1 = NormalContact()
let c3 = BadContact()
c1.address.city = c3.address.city // compiler hides the truth from you and then you sudden get a crash
}
}
struct NormalContact {
var address : Address = Address(city: "defaultCity")
}
struct BadContact {
var address : Address!
}
struct Address {
var city : String
}
เรื่องสั้นขนาดยาวโดยใช้var address : Address!
คุณกำลังซ่อนความเป็นไปได้ที่ตัวแปรอาจมาnil
จากผู้อ่านคนอื่น ๆ และเมื่อมันพังคุณก็จะเหมือน "ห่าอะไร! ของฉันaddress
ไม่ใช่ทางเลือกแล้วทำไมฉันถึงพังล่ะ!.
ดังนั้นจึงควรเขียนดังนี้:
c1.address.city = c2.address!.city // ERROR: Fatal error: Unexpectedly found nil while unwrapping an Optional value
ตอนนี้คุณสามารถบอกฉันได้ไหมว่ามันคือวัตถุชิ้นnil
ไหน?
คราวนี้รหัสได้ชัดเจนมากขึ้นสำหรับคุณ คุณสามารถหาเหตุผลเข้าข้างตนเองและคิดว่าน่าจะเป็นaddress
พารามิเตอร์ที่แกะออกมาอย่างแรง
รหัสเต็มจะเป็น:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var c1 = NormalContact()
let c2 = GoodContact()
c1.address.city = c2.address!.city
c1.address.city = c2.address?.city // not compile-able. No deceiving by the compiler
c1.address.city = c2.address.city // not compile-able. No deceiving by the compiler
if let city = c2.address?.city { // safest approach. But that's not what I'm talking about here.
c1.address.city = city
}
}
}
struct NormalContact {
var address : Address = Address(city: "defaultCity")
}
struct GoodContact {
var address : Address?
}
struct Address {
var city : String
}
ข้อผิดพลาดEXC_BAD_INSTRUCTION
และfatal error: unexpectedly found nil while implicitly unwrapping an Optional value
ปรากฏมากที่สุดเมื่อคุณได้ประกาศ@IBOutlet
แต่ไม่ได้เชื่อมต่อกับสตอรี่บอร์ด
คุณควรเรียนรู้เกี่ยวกับวิธีการทำงานของOptionalsซึ่งกล่าวถึงในคำตอบอื่น ๆ แต่นี่เป็นครั้งเดียวที่ส่วนใหญ่ดูเหมือนกับฉัน
หากคุณได้รับข้อผิดพลาดนี้ใน CollectionView ให้ลองสร้างไฟล์ CustomCell และ Custom xib ด้วย
เพิ่มรหัสนี้ใน ViewDidLoad () ที่ mainVC
let nib = UINib(nibName: "CustomnibName", bundle: nil)
self.collectionView.register(nib, forCellWithReuseIdentifier: "cell")
ฉันพบข้อผิดพลาดนี้ขณะทำการต่อจากตัวควบคุมมุมมองตารางไปยังตัวควบคุมมุมมองเนื่องจากฉันลืมระบุชื่อคลาสที่กำหนดเองสำหรับตัวควบคุมมุมมองในกระดานเรื่องราวหลัก
สิ่งง่ายๆที่ควรค่าแก่การตรวจสอบว่าสิ่งอื่น ๆ นั้นใช้ได้หรือไม่