เห็นแก่ความสอดคล้องกันมันจะสมเหตุสมผลหรือไม่ที่เราจะสามารถห่อโค้ดของเราด้วยการจัดการข้อผิดพลาดโดยไม่จำเป็นต้อง refactor
เพื่อที่จะตอบนี้ก็จำเป็นต้องดูที่มากกว่าเพียงแค่ขอบเขตของตัวแปร
แม้ว่าตัวแปรยังคงอยู่ในขอบเขตก็จะไม่ได้รับการมอบหมายแน่นอน
การประกาศตัวแปรใน try block เป็นการแสดงออกถึงคอมไพเลอร์และต่อผู้อ่านมนุษย์ว่ามันมีความหมายเฉพาะในบล็อกนั้น มันมีประโยชน์สำหรับคอมไพเลอร์ในการบังคับใช้
หากคุณต้องการให้ตัวแปรอยู่ในขอบเขตหลังจากลองบล็อกคุณสามารถประกาศตัวแปรภายนอกบล็อก:
var zerothVariable = 1_000_000_000_000L;
int firstVariable;
try {
// Change checked to unchecked to allow the overflow without throwing.
firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
นั่นเป็นการแสดงออกว่าตัวแปรอาจมีความหมายนอกบล็อกการลอง คอมไพเลอร์จะอนุญาตสิ่งนี้
แต่มันก็แสดงให้เห็นเหตุผลอื่นที่มักจะไม่เป็นประโยชน์ในการรักษาตัวแปรในขอบเขตหลังจากแนะนำพวกเขาในลองบล็อก คอมไพเลอร์ C # ดำเนินการวิเคราะห์การกำหนดที่แน่นอนและห้ามมิให้อ่านค่าของตัวแปรที่ไม่ได้พิสูจน์ว่าได้รับค่าแล้ว ดังนั้นคุณยังไม่สามารถอ่านจากตัวแปรได้
สมมติว่าฉันพยายามอ่านจากตัวแปรหลังจากลองบล็อก:
Console.WriteLine(firstVariable);
นั่นจะทำให้เกิดข้อผิดพลาดในการคอมไพล์เวลา :
CS0165 การใช้ตัวแปรท้องถิ่นที่ไม่ได้กำหนด 'firstVariable'
ฉันเรียกว่าEnvironment.Exitใน catch block ดังนั้นฉันรู้ว่าตัวแปรได้รับการกำหนดก่อนการเรียกไปยัง Console.WriteLine แต่คอมไพเลอร์ไม่ได้อนุมานเรื่องนี้
ทำไมผู้แปลถึงเข้มงวดมาก?
ฉันไม่สามารถทำสิ่งนี้ได้:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
วิธีหนึ่งในการดูข้อ จำกัด นี้คือการวิเคราะห์การกำหนดที่ชัดเจนใน C # นั้นไม่ซับซ้อนมากนัก แต่อีกวิธีหนึ่งในการดูมันคือเมื่อคุณเขียนโค้ดในบล็อกลองด้วยคำสั่ง catch คุณกำลังบอกทั้งคอมไพเลอร์และผู้อ่านคนใด ๆ ว่าควรได้รับการปฏิบัติเหมือนไม่สามารถรันได้ทั้งหมด
เพื่อแสดงให้เห็นว่าสิ่งที่ผมหมายถึงคิดว่าคอมไพเลอร์ที่ได้รับอนุญาตโค้ดข้างต้น แต่แล้วคุณเพิ่มการโทรในบล็อกลองให้ฟังก์ชั่นที่คุณเองรู้ว่าจะไม่โยนข้อยกเว้น ไม่สามารถรับประกันได้ว่าฟังก์ชั่นที่เรียกใช้ไม่ได้โยนIOException
คอมไพเลอร์ไม่สามารถรู้ได้ว่าn
ได้รับมอบหมายจากนั้นคุณจะต้องปรับโครงสร้างใหม่
นี่คือการกล่าวโดยการวิเคราะห์ขั้นสูงในการพิจารณาว่าตัวแปรที่กำหนดในการลองบล็อกด้วย catch clauses ได้รับการกำหนดแน่นอนในภายหลังผู้แปลคอมไพเลอร์ช่วยให้คุณหลีกเลี่ยงการเขียนโค้ดที่มีแนวโน้มที่จะทำลายในภายหลัง (ท้ายที่สุดการจับข้อยกเว้นมักจะหมายความว่าคุณคิดว่าอาจถูกโยนทิ้ง)
คุณสามารถตรวจสอบให้แน่ใจว่าตัวแปรถูกกำหนดผ่านเส้นทางโค้ดทั้งหมด
คุณสามารถสร้างโค้ดคอมไพล์โดยให้ค่าตัวแปรก่อนหน้าบล็อกลองหรือใน catch block ด้วยวิธีนี้มันจะยังคงได้รับการเริ่มต้นหรือกำหนดแม้ว่าการมอบหมายในบล็อกลองไม่เกิดขึ้น ตัวอย่างเช่น:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
หรือ:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
รวบรวมเหล่านั้น แต่เป็นการดีที่สุดที่จะทำอะไรแบบนั้นถ้าค่าเริ่มต้นที่คุณให้มันสมเหตุสมผล*และสร้างพฤติกรรมที่ถูกต้อง
โปรดทราบว่าในกรณีที่สองนี้ที่คุณกำหนดตัวแปรในลองบล็อกและบล็อก catch ทั้งหมดแม้ว่าคุณสามารถอ่านตัวแปรหลังจากลอง catch คุณยังคงไม่สามารถอ่านตัวแปรภายในfinally
บล็อกที่แนบเนื่องจากการดำเนินการสามารถออกจากบล็อกลองในสถานการณ์เกินกว่าที่เรามักจะคิดเกี่ยวกับ
* อย่างไรก็ตามบางภาษาเช่น C และ C ++ ทั้งคู่อนุญาตตัวแปรที่ไม่มีการกำหนดค่าเริ่มต้นและไม่มีการวิเคราะห์การกำหนดที่แน่นอนเพื่อป้องกันการอ่านจากพวกเขา เนื่องจากการอ่านหน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นทำให้โปรแกรมทำงานในลักษณะnondeterministic และไม่อยู่กับร่องกับรอยจึงแนะนำให้หลีกเลี่ยงการแนะนำตัวแปรในภาษาเหล่านั้นโดยไม่ต้องใช้ initializer ในภาษาที่มีการวิเคราะห์การมอบหมายที่ชัดเจนเช่น C # และ Java คอมไพเลอร์ช่วยให้คุณประหยัดจากการอ่านตัวแปรที่ไม่ได้กำหนดค่าเริ่มต้นและจากความชั่วที่น้อยกว่าในการเริ่มต้นพวกเขาด้วยค่าที่ไม่มีความหมาย
คุณสามารถทำให้มันเป็นเส้นทางรหัสที่ตัวแปรไม่ได้รับมอบหมายโยนข้อยกเว้น (หรือกลับ)
หากคุณวางแผนที่จะดำเนินการบางอย่าง (เช่นการบันทึก) และสร้างข้อยกเว้นอีกครั้งหรือโยนข้อยกเว้นอื่นและสิ่งนี้จะเกิดขึ้นในส่วนคำสั่ง catch ใด ๆ ที่ไม่ได้กำหนดตัวแปรไว้คอมไพเลอร์จะทราบว่ามีการกำหนดตัวแปรแล้ว:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
การรวบรวมนั้นและอาจเป็นทางเลือกที่สมเหตุสมผล อย่างไรก็ตามในการประยุกต์ใช้จริงเว้นแต่ข้อยกเว้นจะโยนเฉพาะในสถานการณ์ที่มันไม่ได้ทำให้ความรู้สึกที่จะพยายามกู้คืน*คุณควรตรวจสอบให้แน่ใจว่าคุณจะยังคงจับและถูกต้องจัดการกับมันอยู่ที่ไหนสักแห่ง
(คุณไม่สามารถอ่านตัวแปรในช่วงท้ายบล็อกในสถานการณ์นี้ได้เช่นกัน แต่ก็ไม่รู้สึกว่าคุณควรจะสามารถ - หลังจากทั้งหมดบล็อกสุดท้ายจะทำงานเป็นหลักเสมอและในกรณีนี้ตัวแปรจะไม่ได้รับมอบหมายเสมอไป .)
* ตัวอย่างเช่นแอปพลิเคชันจำนวนมากไม่มี catch clause ที่จัดการOutOfMemoryExceptionเนื่องจากสิ่งที่พวกเขาสามารถทำได้เกี่ยวกับมันอาจเป็นอย่างน้อยก็แย่ไม่แพ้กัน
บางทีคุณจริงๆไม่ต้องการที่จะ refactor รหัส
ในตัวอย่างของคุณคุณแนะนำfirstVariable
และsecondVariable
ลองบล็อก ดังที่ฉันได้กล่าวไว้คุณสามารถกำหนดพวกเขาก่อนที่จะลองบล็อกที่พวกเขาได้รับมอบหมายดังนั้นพวกเขาจะยังคงอยู่ในขอบเขตหลังจากนั้นและคุณสามารถตอบสนอง / หลอกลวงคอมไพเลอร์ในการอนุญาตให้คุณอ่านจากพวกเขา
แต่รหัสที่ปรากฏหลังจากบล็อกเหล่านั้นน่าจะขึ้นอยู่กับพวกเขาได้รับมอบหมายอย่างถูกต้อง หากเป็นกรณีนี้รหัสของคุณควรสะท้อนและตรวจสอบให้แน่ใจ
ก่อนอื่นคุณสามารถจัดการกับข้อผิดพลาดที่นั่นได้หรือไม่ หนึ่งในสาเหตุของการจัดการข้อยกเว้นคือเพื่อให้ง่ายต่อการจัดการข้อผิดพลาดที่พวกเขาสามารถจัดการได้อย่างมีประสิทธิภาพแม้ว่าจะไม่ได้อยู่ใกล้ที่ที่พวกเขาเกิดขึ้น
หากคุณไม่สามารถจัดการกับข้อผิดพลาดในฟังก์ชั่นที่เริ่มต้นและใช้ตัวแปรเหล่านั้นได้บางทีบล็อกลองไม่ควรอยู่ในฟังก์ชันนั้นเลย แต่แทนที่จะอยู่ที่อื่นที่สูงกว่า (เช่นในรหัสที่เรียกใช้ฟังก์ชันนั้นหรือรหัส ที่เรียกรหัสนั้น ) เพียงให้แน่ใจว่าคุณไม่ได้ตั้งใจจับยกเว้นโยนที่อื่นและผิดสมมติว่ามันถูกโยนลงในขณะที่การเริ่มต้นและfirstVariable
secondVariable
อีกวิธีคือใส่รหัสที่ใช้ตัวแปรในบล็อกลอง สิ่งนี้มักจะสมเหตุสมผล อีกครั้งหากข้อยกเว้นเดียวกันกับที่คุณจับได้จาก initializers อาจถูกโยนจากรหัสที่อยู่รอบ ๆ คุณควรตรวจสอบให้แน่ใจว่าคุณไม่ละเลยความเป็นไปได้นั้นเมื่อจัดการกับมัน
(ฉันสมมติว่าคุณกำลังเริ่มต้นตัวแปรด้วยนิพจน์ที่ซับซ้อนกว่าที่แสดงในตัวอย่างของคุณเช่นพวกเขาสามารถโยนข้อยกเว้นได้จริงและคุณยังไม่ได้วางแผนที่จะจับข้อยกเว้นที่เป็นไปได้ทั้งหมดแต่เพียงจับข้อยกเว้นเฉพาะใด ๆคุณสามารถคาดหวังและจัดการอย่างมีความหมายมันเป็นความจริงที่โลกแห่งความจริงไม่ได้ดีนักและบางครั้งรหัสการผลิตก็ทำเช่นนี้แต่เนื่องจากเป้าหมายของคุณที่นี่คือการจัดการข้อผิดพลาดที่เกิดขึ้นในขณะเริ่มต้นตัวแปรเฉพาะสองประการ จุดประสงค์ควรเฉพาะเจาะจงกับข้อผิดพลาดใด ๆ ที่เกิดขึ้น)
วิธีที่สามคือการแยกรหัสที่สามารถล้มเหลวและลองจับที่จัดการมันเป็นวิธีการของตัวเอง สิ่งนี้มีประโยชน์หากคุณอาจต้องการจัดการกับข้อผิดพลาดอย่างสมบูรณ์ก่อนจากนั้นไม่ต้องกังวลกับการจับข้อยกเว้นโดยไม่ตั้งใจซึ่งควรจัดการที่อื่นแทน
ตัวอย่างเช่นสมมติว่าคุณต้องการออกจากแอปพลิเคชันทันทีเมื่อไม่สามารถกำหนดตัวแปรได้ (เห็นได้ชัดว่าไม่ใช่การจัดการข้อยกเว้นทั้งหมดสำหรับข้อผิดพลาดร้ายแรงนี่เป็นเพียงตัวอย่างและอาจใช่หรือไม่ใช่วิธีที่คุณต้องการให้แอปพลิเคชันของคุณตอบสนองต่อปัญหา) คุณสามารถทำสิ่งนี้:
// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
try {
// This code is contrived. The idea here is that obtaining the values
// could actually fail, and throw a SomeSpecificException.
var firstVariable = 1;
var secondVariable = firstVariable;
return (firstVariable, secondVariable);
}
catch (SomeSpecificException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
throw new InvalidOperationException(); // unreachable
}
}
// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
var (firstVariable, secondVariable) = GetFirstAndSecondValues();
// Code that does something with them...
}
รหัสนั้นจะคืนค่าและแยกโครงสร้าง ValueTuple ด้วยไวยากรณ์ของ C # 7.0เพื่อคืนค่าหลายค่า แต่ถ้าคุณยังคงใช้ C # รุ่นก่อนหน้าคุณยังคงสามารถใช้เทคนิคนี้ได้ ตัวอย่างเช่นคุณสามารถใช้พารามิเตอร์หรือกลับวัตถุที่กำหนดเองที่ให้ทั้งสองค่า นอกจากนี้หากตัวแปรสองตัวนั้นไม่ได้เกี่ยวข้องกันอย่างแน่นหนาน่าจะมีวิธีแยกกันสองวิธี
โดยเฉพาะอย่างยิ่งหากคุณมีหลายวิธีเช่นนั้นคุณควรพิจารณาการรวมรหัสของคุณไว้ที่ส่วนกลางเพื่อแจ้งให้ผู้ใช้ทราบถึงข้อผิดพลาดร้ายแรงและการเลิกใช้ (ตัวอย่างเช่นคุณสามารถเขียนDie
วิธีการที่มีmessage
พารามิเตอร์.) บรรทัดจะไม่ดำเนินการจริงเพื่อให้คุณไม่จำเป็น (และไม่ควร) เขียนประโยคจับมันthrow new InvalidOperationException();
นอกเหนือจากการเลิกสูบบุหรี่เมื่อข้อผิดพลาดเกิดขึ้นโดยเฉพาะอย่างยิ่งคุณอาจรหัสบางครั้งการเขียนที่มีลักษณะเช่นนี้ถ้าคุณโยนข้อยกเว้นชนิดอื่นที่ wraps ยกเว้นเดิม (ในสถานการณ์นั้นคุณไม่จำเป็นต้องใช้นิพจน์การขว้างปาที่สองและไม่สามารถเข้าถึงได้)
สรุป: ขอบเขตเป็นเพียงส่วนหนึ่งของภาพ
คุณสามารถบรรลุผลของการห่อรหัสของคุณด้วยการจัดการข้อผิดพลาดโดยไม่ต้อง refactoring (หรือถ้าคุณต้องการด้วยแทบ refactoring ใด ๆ ) เพียงแค่แยกการประกาศของตัวแปรจากการกำหนดของพวกเขา คอมไพเลอร์อนุญาตให้ทำได้หากคุณปฏิบัติตามกฎการกำหนดที่ชัดเจนของ C # และประกาศตัวแปรล่วงหน้าของบล็อก try ทำให้ขอบเขตใหญ่ขึ้น แต่การปรับโครงสร้างอีกครั้งอาจเป็นตัวเลือกที่ดีที่สุดของคุณ
try.. catch
คือรหัสบล็อกประเภทหนึ่งและตราบใดที่บล็อคโค้ดทั้งหมดไปคุณไม่สามารถประกาศตัวแปรหนึ่งและใช้ตัวแปรเดียวกันนั้นในอีกขอบเขตหนึ่งได้