ไม่จำเป็นต้องกำหนดพารามิเตอร์ประเภทโครงสร้างออก


9

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

ฉันมีคลาสทดสอบนี้แล้ว:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

ไม่มีการมอบหมายให้กับอีเมลGetOutซึ่งโดยปกติจะทำให้เกิดข้อผิดพลาด:

ต้องกำหนดพารามิเตอร์ 'อีเมล' ก่อนจึงจะออกจากวิธีการปัจจุบัน

อย่างไรก็ตามหาก EmailAddress อยู่ในโครงสร้างในแอสเซมบลีที่แยกจากกันจะไม่มีข้อผิดพลาดเกิดขึ้น

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

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


2
หากคุณกำลังใช้คลาสคุณต้อง 'อินสแตนซ์' ของอินสแตนซ์ของวัตถุ มันไม่จำเป็นสำหรับ structs docs.microsoft.com/en-us/dotnet/csharp/programming-guide/ ...... (ค้นหาข้อความนี้ในหน้านั้นโดยเฉพาะ: ไม่เหมือนคลาส, structs สามารถอินสแตนซ์ได้โดยไม่ต้องใช้ operato ใหม่)
Dortimer

1
ทันทีที่สุนัข Dog ของคุณได้รับตัวแปรมันจะไม่คอมไพล์ :)
André Sanson

ในตัวอย่างนี้ด้วยstruct Dog{}ทั้งหมดเป็นอย่างดี
Henk Holterman

2
@ johnny5 จากนั้นแสดงตัวอย่าง
André Sanson

1
ตกลงนี่น่าสนใจ สร้างซ้ำด้วยแอป Core 3 Console และ lib .Standard class lib
Henk Holterman

คำตอบ:


12

TLDR: นี่เป็นข้อผิดพลาดที่รู้จักกันมานาน ฉันแรกเขียนเกี่ยวกับเรื่องนี้ในปี 2010:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

มันไม่เป็นอันตรายและคุณสามารถเพิกเฉยได้อย่างปลอดภัยและขอแสดงความยินดีกับการค้นหาบั๊กที่ค่อนข้างคลุมเครือ

ทำไมคอมไพเลอร์ไม่บังคับใช้ที่Emailต้องกำหนดอย่างแน่นอน?

โอ้มันเป็นไปตามแฟชั่น มันมีความคิดผิด ๆ ว่าเงื่อนไขใดที่บอกเป็นนัย ๆ ว่าตัวแปรนั้นถูกกำหนดอย่างแน่นอนตามที่เราจะได้เห็น

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

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

พิจารณาสิ่งนี้:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

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

เราต้องการอะไร เราต้องการให้fมีการมอบหมายอย่างแน่นอน ณ จุดใดก็ตามที่การควบคุมออกไปMตามปกติ ดังนั้นคุณจะคาดหวังสิ่งที่ชอบ:

void M(out Foo f)
{
  f = new Foo();
}

ซึ่งกำหนดf.xและf.yเป็นค่าเริ่มต้น แต่แล้วเรื่องนี้ล่ะ

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

นั่นก็ควรจะดี แต่และนี่คือนักเตะเหตุใดเราจึงต้องกำหนดค่าเริ่มต้นให้ระเบิดออกไปในภายหลัง ตัวตรวจสอบการกำหนดที่ชัดเจนของ C # จะตรวจสอบเพื่อดูว่ามีการกำหนดทุกฟิลด์หรือไม่! สิ่งนี้ถูกกฎหมาย:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

และทำไมจึงไม่ถูกกฎหมาย? มันเป็นประเภทค่า fเป็นตัวแปรและมันมีค่าประเภทที่ถูกต้องอยู่แล้วFooดังนั้นลองตั้งค่าเขตข้อมูลและเราเสร็จแล้วใช่ไหม

ขวา. ดังนั้นบั๊กคืออะไร

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

และตอนนี้คุณน่าจะสามารถสรุปสาเหตุของข้อผิดพลาดที่คุณพบ เมื่อคอมไพเลอร์ตรวจสอบว่าพารามิเตอร์ out ได้รับการกำหนดแน่นอนจะทำการเปรียบเทียบจำนวนของเขตข้อมูลที่รู้จักกับจำนวนของเขตข้อมูลที่ได้รับการกำหนดค่าเริ่มต้นแน่นอนและในกรณีของคุณจะรู้เกี่ยวกับเขตข้อมูลสาธารณะเป็นศูนย์เท่านั้น. คอมไพเลอร์สรุป "จำเป็นต้องกรอกข้อมูลในฟิลด์ให้เป็นศูนย์, เริ่มต้นเป็นศูนย์แล้ว, เราทำได้ดี"

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

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


@ johnny5: คุณไม่ควรรับข้อผิดพลาด ดูdotnetfiddle.net/ZEKiUk คุณสามารถโพสต์ซ้ำง่ายๆได้หรือไม่
Eric Lippert

1
ขอบคุณสำหรับซอมันเป็นเพราะฉันกำหนด x และ y เป็นคุณสมบัติแทนสมาชิก
johnny 5

1
@ johnny5: หากคุณเพิ่งกำหนดคุณสมบัติสไตล์ C # 1.0 ตามปกติจากมุมมองของตัวตรวจสอบการกำหนดที่แน่นอนนั่นเป็นวิธีการไม่ใช่ฟิลด์ หากคุณกำหนดคุณสมบัติอัตโนมัติของสไตล์ C # 3.0 ขึ้นไปคอมไพเลอร์รู้ว่ามีฟิลด์ส่วนตัวสำรองอยู่ กฎสำหรับการมอบหมายที่ชัดเจนของสิ่งนั้นได้รับการปรับเปลี่ยนในช่วงหลายปีที่ผ่านมาและตอนนี้ฉันไม่จำกฎที่แน่นอนได้
Eric Lippert

ถ้าคุณใช้System.TimeSpanแทนข้อผิดพลาดที่ไม่มาและerror CS0269: Use of unassigned out parameter 'email' error CS0177: The out parameter 'email' must be assigned to before control leaves the current methodมีเพียงหนึ่งฟิลด์ไม่คงที่คือคือTimeSpan _ticksมันคือinternalการชุมนุม mscorlib การชุมนุมครั้งนี้เป็นพิเศษหรือไม่? ก็เช่นกันSystem.DateTimeและทุ่งนาก็คือprivate
Jeppe Stig Nielsen

@JeppeStigNielsen: ฉันไม่รู้ว่าเกิดอะไรขึ้นกับมัน! หากคุณคิดออกโปรดแจ้งให้เราทราบ
Eric Lippert

1

ในขณะที่ดูเหมือนว่าข้อผิดพลาดมันทำให้รู้สึกบางอย่าง

'ข้อผิดพลาดที่ขาดหายไป' ปรากฏขึ้นเฉพาะเมื่อใช้ไลบรารีคลาส และไลบรารีคลาสอาจถูกเขียนด้วยภาษา. net อื่นเช่น VB.Net 'การติดตามการมอบหมายที่ชัดเจน' เป็นคุณลักษณะของ C # ไม่ใช่ของเฟรมเวิร์ก

ดังนั้นโดยรวมฉันไม่คิดว่ามันเป็นข้อผิดพลาด แต่ฉันไม่รู้เกี่ยวกับข้อความสั่งอนุญาตนี้


หากคุณกำลังนำเข้าแอสเซมบลีไปยัง C # แม้ว่าโครงสร้างอาจอยู่ในแอสเซมบลีที่เขียนในภาษาอื่นรหัสที่ใช้ในแอปพลิเคชันยังคงเป็น c # ดังนั้นไม่ควรใช้การติดตามการกำหนดที่แน่นอน
johnny 5

1
ไม่จำเป็น. C # จะไม่อนุญาตให้คุณใช้ตัวแปรหน่วย (ท้องถิ่น) แต่ในขณะเดียวกันเฟรมเวิร์กจะรับประกันว่ามันจะถูกตั้งค่าเป็น 0 ( default(T)) ดังนั้นจึงไม่มีการละเมิดความปลอดภัยของหน่วยความจำหรือสิ่งที่คล้ายกัน
Henk Holterman

3
ฉันสามารถทำให้คำสั่งที่มีสิทธิ์ :) มันเป็นบั๊กที่รู้จักมายาวนาน
Eric Lippert

1
@EricLippert ขอบคุณฉันหวังว่าคุณจะเห็นสิ่งนี้ในบางจุด
johnny 5
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.