ฉันสามารถเดาได้อย่างสมเหตุสมผลว่าเกิดอะไรขึ้นที่นี่ แต่มันซับซ้อนไปหน่อย :) มันเกี่ยวข้องกับสถานะว่างเปล่าและการติดตามค่า null ที่อธิบายไว้ในข้อมูลจำเพาะฉบับร่าง โดยพื้นฐานแล้ว ณ จุดที่เราต้องการส่งคืนคอมไพเลอร์จะเตือนว่าสถานะของนิพจน์นั้นเป็น "อาจเป็นโมฆะ" แทนที่จะเป็น "ไม่เป็นโมฆะ"
คำตอบนี้อยู่ในรูปแบบการเล่าเรื่องค่อนข้างมากกว่า "นี่คือข้อสรุป" ... ฉันหวังว่ามันจะมีประโยชน์มากกว่านี้
ฉันจะทำให้ตัวอย่างง่ายขึ้นเล็กน้อยโดยกำจัดฟิลด์และพิจารณาเมธอดที่มีหนึ่งในสองลายเซ็นต์:
public static string M(string? text)
public static string M(string text)
ในการใช้งานด้านล่างฉันได้กำหนดหมายเลขที่แตกต่างกันในแต่ละวิธีเพื่อให้ฉันสามารถอ้างอิงตัวอย่างที่ชัดเจนได้อย่างไม่น่าสงสัย นอกจากนี้ยังช่วยให้การใช้งานทั้งหมดจะปรากฏในโปรแกรมเดียวกัน
ในแต่ละกรณีที่อธิบายไว้ด้านล่างเราจะทำสิ่งต่าง ๆ แต่จบลงด้วยความพยายามที่จะกลับมาtext
- ดังนั้นจึงเป็นสถานะว่างเปล่าtext
เปล่าที่สำคัญ
ผลตอบแทนที่ไม่มีเงื่อนไข
อันดับแรกให้ลองส่งคืนโดยตรง:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
จนถึงตอนนี้ง่ายมาก สถานะที่เป็นโมฆะของพารามิเตอร์ที่จุดเริ่มต้นของวิธีการคือ "อาจเป็นโมฆะ" หากเป็นประเภทstring?
และ "ไม่เป็นโมฆะ" หากเป็นประเภทstring
ถ้าหากมันเป็นประเภท
ผลตอบแทนตามเงื่อนไขง่าย ๆ
ตอนนี้เรามาตรวจสอบ null ภายในif
เงื่อนไข statement (ฉันจะใช้โอเปอเรเตอร์ที่มีเงื่อนไขซึ่งฉันเชื่อว่าจะมีผลเหมือนกัน แต่ฉันอยากอยู่กับคำถามที่แท้จริงยิ่งขึ้น)
public static string M3(string? text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
public static string M4(string text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
ยอดเยี่ยมดังนั้นดูเหมือนว่าภายในif
ข้อความที่เงื่อนไขตรวจสอบความว่างเปล่าสถานะของตัวแปรภายในแต่ละสาขาของif
คำสั่งอาจแตกต่างกัน: ภายในelse
บล็อกสถานะเป็น "ไม่เป็นโมฆะ" ในโค้ดทั้งสองชิ้น ดังนั้นโดยเฉพาะอย่างยิ่งใน M3 สถานะจะเปลี่ยนจาก "อาจเป็นโมฆะ" เป็น "ไม่เป็นโมฆะ"
การส่งคืนแบบมีเงื่อนไขพร้อมตัวแปรโลคัล
ทีนี้ลองยกเงื่อนไขนั้นไปที่ตัวแปรท้องถิ่น:
public static string M5(string? text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
public static string M6(string text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
คำเตือนปัญหาทั้ง M5 และ M6 ดังนั้นไม่เพียง แต่เราจะไม่ได้รับผลกระทบเชิงบวกจากการเปลี่ยนสถานะจาก "อาจเป็นโมฆะ" เป็น "ไม่เป็นโมฆะ" ใน M5 (เช่นเดียวกับใน M3) ... เราได้ตรงกันข้ามผลใน M6 ซึ่งรัฐเปลี่ยนไป " ไม่ใช่โมฆะ "เป็น" อาจเป็นโมฆะ " นั่นทำให้ฉันประหลาดใจจริงๆ
ดังนั้นดูเหมือนว่าเราได้เรียนรู้ว่า:
- ตรรกะรอบ ๆ "วิธีคำนวณตัวแปรท้องถิ่น" ไม่ได้ใช้เพื่อเผยแพร่ข้อมูลสถานะ เพิ่มเติมในภายหลัง
- การแนะนำการเปรียบเทียบแบบ null สามารถเตือนคอมไพเลอร์ว่าสิ่งที่ก่อนหน้านี้คิดว่าไม่ใช่ null อาจเป็นโมฆะ
ผลตอบแทนที่ไม่มีเงื่อนไขหลังจากการเปรียบเทียบที่ถูกละเว้น
ลองดูที่สองของสัญลักษณ์แสดงหัวข้อย่อยเหล่านั้นโดยการแนะนำการเปรียบเทียบก่อนที่จะกลับมาอย่างไม่มีเงื่อนไข (ดังนั้นเราจึงไม่สนใจผลการเปรียบเทียบอย่างสมบูรณ์):
public static string M7(string? text)
{
bool ignored = text is null;
return text; // Warning
}
public static string M8(string text)
{
bool ignored = text is null;
return text; // Warning
}
โปรดสังเกตว่า M8 รู้สึกอย่างไรว่าควรเทียบเท่ากับ M2 - ทั้งคู่มีพารามิเตอร์ที่ไม่เป็นโมฆะซึ่งพวกเขากลับมาโดยไม่มีเงื่อนไข - แต่การแนะนำการเปรียบเทียบด้วย null จะเปลี่ยนสถานะจาก "ไม่ใช่โมฆะ" เป็น "อาจเป็นโมฆะ" เราสามารถรับหลักฐานเพิ่มเติมเกี่ยวกับสิ่งนี้ได้โดยพยายามอ้างเหตุผลtext
ก่อนเงื่อนไข:
public static string M9(string text)
{
int length1 = text.Length; // No warning
bool ignored = text is null;
int length2 = text.Length; // Warning
return text; // No warning
}
โปรดสังเกตว่าreturn
คำสั่งไม่มีคำเตือนในขณะนี้: สถานะหลังจากดำเนินการtext.Length
คือ "ไม่เป็นโมฆะ" (เพราะถ้าเราดำเนินการนิพจน์นั้นสำเร็จจะไม่เป็นโมฆะ) ดังนั้นtext
พารามิเตอร์เริ่มต้นเป็น "ไม่เป็นโมฆะ" เนื่องจากชนิดของมันกลายเป็น "อาจเป็นโมฆะ" เนื่องจากการเปรียบเทียบเป็นโมฆะจากนั้นกลายเป็น "ไม่เป็นโมฆะ" อีกครั้งหลังจากtext2.Length
นั้น
การเปรียบเทียบอะไรส่งผลกระทบต่อรัฐ
นั่นคือการเปรียบเทียบtext is null
... การเปรียบเทียบที่คล้ายกันมีผลอย่างไร ต่อไปนี้เป็นวิธีการสี่วิธีทั้งหมดเริ่มต้นด้วยพารามิเตอร์สตริงที่ไม่ใช่ค่า null:
public static string M10(string text)
{
bool ignored = text == null;
return text; // Warning
}
public static string M11(string text)
{
bool ignored = text is object;
return text; // No warning
}
public static string M12(string text)
{
bool ignored = text is { };
return text; // No warning
}
public static string M13(string text)
{
bool ignored = text != null;
return text; // Warning
}
ดังนั้นแม้ว่าx is object
ในขณะนี้คือทางเลือกที่แนะนำให้x != null
พวกเขาไม่ได้มีผลเช่นเดียวกันเพียง แต่เปรียบเทียบกับ null (ใด ๆis
, ==
หรือ!=
) เปลี่ยนสถานะจาก "ไม่เป็นโมฆะ" กับ "บางที null"
เหตุใดการยกเงื่อนไขจึงมีผล
กลับไปที่กระสุนแรกของเราก่อนหน้านี้เหตุใด M5 และ M6 จึงไม่คำนึงถึงเงื่อนไขที่นำไปสู่ตัวแปรท้องถิ่น สิ่งนี้ไม่ได้ทำให้ฉันประหลาดใจเท่าที่ดูเหมือนจะทำให้คนอื่นประหลาดใจ การสร้างตรรกะแบบนั้นลงในคอมไพเลอร์และสเปคนั้นเป็นงานจำนวนมากและเพื่อประโยชน์ค่อนข้างน้อย นี่เป็นอีกตัวอย่างหนึ่งที่ไม่มีส่วนเกี่ยวข้องกับความสามารถในการลบล้างสิ่งที่อินไลน์มีผล:
public static int X1()
{
if (true)
{
return 1;
}
}
public static int X2()
{
bool alwaysTrue = true;
if (alwaysTrue)
{
return 1;
}
// Error: not all code paths return a value
}
แม้ว่าเราจะรู้ว่ามันalwaysTrue
จะเป็นจริงเสมอ แต่ก็ไม่เป็นไปตามข้อกำหนดในสเปคที่ทำให้โค้ดหลังจากif
คำสั่งไม่สามารถเข้าถึงได้ซึ่งเป็นสิ่งที่เราต้องการ
นี่เป็นอีกตัวอย่างหนึ่งที่เกี่ยวข้องกับการมอบหมายที่ชัดเจน:
public static void X3()
{
string x;
bool condition = DateTime.UtcNow.Year == 2020;
if (condition)
{
x = "It's 2020.";
}
if (!condition)
{
x = "It's not 2020.";
}
// Error: x is not definitely assigned
Console.WriteLine(x);
}
แม้ว่าเรารู้ว่ารหัสจะเข้าสู่หนึ่งในเนื้อหาของif
คำสั่งเหล่านั้นอย่างแน่นอนแต่ก็ไม่มีอะไรพิเศษในการทำงาน เครื่องมือวิเคราะห์แบบคงที่อาจทำเช่นนั้นได้ แต่การพยายามใส่ลงไปในข้อกำหนดภาษาจะเป็นความคิดที่ไม่ดี IMO - เป็นเรื่องดีสำหรับเครื่องมือวิเคราะห์แบบคงที่ที่จะมีการวิเคราะห์พฤติกรรมทุกประเภทที่สามารถพัฒนาได้ตลอดเวลา แต่ไม่มากนัก สำหรับข้อกำหนดภาษา