“ ข้อผิดพลาดร้ายแรง: พบศูนย์โดยไม่คาดคิดขณะแกะค่าทางเลือก” หมายความว่าอย่างไร

Aug 24 2015

โปรแกรม Swift ของฉันขัดข้องEXC_BAD_INSTRUCTIONและเกิดข้อผิดพลาดที่คล้ายกันอย่างใดอย่างหนึ่งต่อไปนี้ ข้อผิดพลาดนี้หมายความว่าอย่างไรและฉันจะแก้ไขได้อย่างไร

ข้อผิดพลาดร้ายแรง: พบศูนย์โดยไม่คาดคิดขณะคลายค่าตัวเลือก

หรือ

ข้อผิดพลาดร้ายแรง: ไม่พบศูนย์โดยไม่คาดคิดขณะแกะค่าทางเลือกโดยปริยาย


โพสต์นี้มีวัตถุประสงค์เพื่อรวบรวมคำตอบสำหรับปัญหาที่ "ไม่พบโดยไม่คาดคิด" เพื่อไม่ให้กระจัดกระจายและหายาก อย่าลังเลที่จะเพิ่มคำตอบของคุณเองหรือแก้ไขคำตอบวิกิที่มีอยู่

คำตอบ

685 Hamish Aug 24 2015 at 02:10

คำตอบนี้เป็นวิกิพีเดียชุมชน หากคุณคิดว่าสามารถปรับปรุงให้ดีขึ้นได้อย่าลังเลที่จะแก้ไข

ความเป็นมา: อะไรเป็นทางเลือก?

ใน 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มีค่าวิธีนี้จะถูกเรียกใช้ หากไม่เป็นเช่นนั้นจะไม่มีอะไรเลวร้ายเกิดขึ้น - โค้ดจะดำเนินการต่อไป

(ลักษณะนี้คล้ายกับการส่งข้อความไปยังnilObjective-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 stringByAppendingStringto be used if anOptionalStringhave 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?ในบางกรณีที่การจัดการข้อผิดพลาดไม่สำคัญ


ทรัพยากร

65 Sajjon Dec 19 2016 at 20:26

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)")

คำตอบนี้มีวัตถุประสงค์เพื่อให้กระชับเพื่อความเข้าใจทั้งหมดที่ได้รับการยอมรับในการอ่านคำตอบ


ทรัพยากร

40 DuncanC Feb 28 2016 at 21:37

คำถามนี้เกิดขึ้นตลอดเวลาใน 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 เป็นครั้งแรกให้แสร้งทำเป็น "!" อักขระไม่ได้เป็นส่วนหนึ่งของภาษา มีแนวโน้มที่จะทำให้คุณมีปัญหา

13 Tharzeez Nov 17 2017 at 23:15

เนื่องจากคำตอบข้างต้นอธิบายวิธีการเล่นอย่างปลอดภัยด้วย 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 ทั้งหมด

12 QiunCheng Jul 30 2016 at 14:18

ขั้นแรกคุณควรทราบว่าค่าที่เป็นทางเลือกคืออะไร คุณสามารถไปที่The Swift Programming Languageเพื่อดูรายละเอียด

ประการที่สองคุณควรทราบว่าค่าทางเลือกมีสองสถานะ ค่าหนึ่งคือค่าเต็มและอีกค่าหนึ่งเป็นค่าศูนย์ ดังนั้นก่อนที่คุณจะใช้ค่าที่เป็นทางเลือกคุณควรตรวจสอบว่าเป็นสถานะใด

คุณสามารถใช้if let ...หรือguard let ... elseอื่น ๆ

อีกวิธีหนึ่งหากคุณไม่ต้องการตรวจสอบสถานะตัวแปรก่อนการใช้งานคุณสามารถใช้var buildingName = buildingName ?? "buildingName"แทนได้

8 Wissa Apr 13 2018 at 21:06

ฉันมีข้อผิดพลาดนี้ครั้งหนึ่งเมื่อฉันพยายามตั้งค่า 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
}

ฉันหวังว่าคำตอบนี้จะช่วยให้ทุกคนที่มีปัญหาเดียวกันกับที่ฉันพบว่าคำตอบที่ทำเครื่องหมายไว้เป็นแหล่งข้อมูลที่ดีในการทำความเข้าใจตัวเลือกและวิธีการทำงาน แต่ยังไม่ได้แก้ไขปัญหาโดยตรง

7 Cristik Sep 19 2018 at 13:04

โดยทั่วไปคุณพยายามใช้ค่าศูนย์ในสถานที่ที่ Swift อนุญาตเฉพาะค่าที่ไม่ใช่ศูนย์โดยบอกให้คอมไพเลอร์เชื่อใจคุณว่าจะไม่มีค่าศูนย์อยู่ที่นั่นดังนั้นจึงอนุญาตให้แอปของคุณรวบรวมได้

มีหลายสถานการณ์ที่นำไปสู่ข้อผิดพลาดร้ายแรงประเภทนี้:

  1. บังคับแกะ:

    let user = someVariable!
    

    ถ้าsomeVariableเป็นศูนย์คุณจะได้รับความผิดพลาด การแกะแรงจะทำให้คุณได้ย้ายความรับผิดชอบในการตรวจสอบศูนย์จากคอมไพเลอร์มาให้คุณโดยพื้นฐานแล้วการแกะแบบบังคับคุณรับประกันกับคอมไพเลอร์ว่าคุณจะไม่มีค่าศูนย์ที่นั่น และเดาว่าจะเกิดอะไรขึ้นถ้าค่าศูนย์จบลงด้วยsomeVariable?

    วิธีการแก้? ใช้การผูกเพิ่มเติม (aka if-let) ทำการประมวลผลตัวแปรที่นั่น:

    if user = someVariable {
        // do your stuff
    }
    
  2. บังคับ (ลง) ร่าย:

    let myRectangle = someShape as! Rectangle
    

    การบังคับแคสต์จะเป็นการบอกให้คอมไพเลอร์ไม่ต้องกังวลอีกต่อไปเพราะคุณจะมีRectangleอินสแตนซ์อยู่ที่นั่นเสมอ และตราบใดที่ถือได้ว่าคุณไม่ต้องกังวล ปัญหาเริ่มต้นเมื่อคุณหรือเพื่อนร่วมงานของคุณจากโครงการเริ่มหมุนเวียนค่าที่ไม่ใช่สี่เหลี่ยมผืนผ้า

    วิธีการแก้? ใช้การผูกเพิ่มเติม (aka if-let) ทำการประมวลผลตัวแปรที่นั่น:

    if let myRectangle = someShape as? Rectangle {
        // yay, I have a rectangle
    }
    
  3. ตัวเลือกที่ไม่ได้ปิดโดยปริยาย สมมติว่าคุณมีนิยามคลาสดังต่อไปนี้:

    class User {
        var name: String!
    
        init() {
            name = "(unnamed)"
        }
    
        func nicerName() {
            return "Mr/Ms " + name
        }
    }
    

    ตอนนี้หากไม่มีใครยุ่งกับnameคุณสมบัติโดยการตั้งค่าเป็นมันnilก็จะทำงานได้ตามที่คาดไว้อย่างไรก็ตามหากUserเริ่มต้นจาก JSON ที่ไม่มีnameคีย์คุณจะได้รับข้อผิดพลาดร้ายแรงเมื่อพยายามใช้คุณสมบัติ

    วิธีการแก้? อย่าใช้มัน :) เว้นแต่คุณจะแน่ใจ 102% ว่าอสังหาริมทรัพย์นั้นจะมีค่าที่ไม่ใช่ศูนย์เสมอเมื่อถึงเวลาที่ต้องใช้ ในกรณีส่วนใหญ่การแปลงเป็นทางเลือกหรือไม่ใช่ทางเลือกจะใช้ได้ การทำให้ไม่เป็นทางเลือกจะส่งผลให้คอมไพเลอร์ช่วยคุณด้วยการบอกเส้นทางรหัสที่คุณพลาดโดยให้ค่าแก่คุณสมบัตินั้น

  4. เต้าเสียบที่ไม่ได้เชื่อมต่อหรือยังไม่ได้เชื่อมต่อ นี่เป็นกรณีเฉพาะของสถานการณ์สมมติ # 3 โดยทั่วไปคุณมีคลาสที่โหลด XIB ที่คุณต้องการใช้

    class SignInViewController: UIViewController {
    
        @IBOutlet var emailTextField: UITextField!
    }
    

    ตอนนี้หากคุณพลาดการเชื่อมต่อเต้าเสียบจากตัวแก้ไข XIB แอปจะหยุดทำงานทันทีที่คุณต้องการใช้เต้าเสียบ วิธีการแก้? ตรวจสอบให้แน่ใจว่าเสียบปลั๊กไฟทั้งหมดแล้ว หรือใช้ตัว?ดำเนินการกับพวกเขา: emailTextField?.text = "[email protected]". หรือประกาศว่าเต้าเสียบเป็นทางเลือกแม้ว่าในกรณีนี้คอมไพเลอร์จะบังคับให้คุณแกะมันออกจากโค้ดทั้งหมด

  5. ค่าที่มาจาก 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.

2 Honey Nov 01 2018 at 11:39

นี่เป็นความคิดเห็นที่สำคัญมากกว่าและเหตุใดตัวเลือกที่ไม่ได้ปิดโดยปริยายจึงสามารถหลอกลวงได้เมื่อพูดถึงการดีบัก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
}
2 AleMohamad May 21 2019 at 00:34

ข้อผิดพลาดEXC_BAD_INSTRUCTIONและfatal error: unexpectedly found nil while implicitly unwrapping an Optional valueปรากฏมากที่สุดเมื่อคุณได้ประกาศ@IBOutletแต่ไม่ได้เชื่อมต่อกับสตอรี่บอร์ด

คุณควรเรียนรู้เกี่ยวกับวิธีการทำงานของOptionalsซึ่งกล่าวถึงในคำตอบอื่น ๆ แต่นี่เป็นครั้งเดียวที่ส่วนใหญ่ดูเหมือนกับฉัน

2 ShigaSuresh Feb 13 2020 at 01:16

หากคุณได้รับข้อผิดพลาดนี้ใน CollectionView ให้ลองสร้างไฟล์ CustomCell และ Custom xib ด้วย

เพิ่มรหัสนี้ใน ViewDidLoad () ที่ mainVC

    let nib = UINib(nibName: "CustomnibName", bundle: nil)
    self.collectionView.register(nib, forCellWithReuseIdentifier: "cell")
MikeVolmar Sep 26 2019 at 22:11

ฉันพบข้อผิดพลาดนี้ขณะทำการต่อจากตัวควบคุมมุมมองตารางไปยังตัวควบคุมมุมมองเนื่องจากฉันลืมระบุชื่อคลาสที่กำหนดเองสำหรับตัวควบคุมมุมมองในกระดานเรื่องราวหลัก

สิ่งง่ายๆที่ควรค่าแก่การตรวจสอบว่าสิ่งอื่น ๆ นั้นใช้ได้หรือไม่