พฤติกรรมการแปลงโดยนัยที่กำหนดเองโดยไม่ระบุตัวตน


542

หมายเหตุ: สิ่งนี้ดูเหมือนจะได้รับการแก้ไขในRoslyn

คำถามนี้เกิดขึ้นเมื่อการเขียนคำตอบของฉันคนนี้ซึ่งพูดคุยเกี่ยวกับการเชื่อมโยงกันของผู้ประกอบการด้วย null coalescing

เช่นเดียวกับการเตือนความคิดของตัวดำเนินการ null-coalescing คือการแสดงออกของแบบฟอร์ม

x ?? y

ประเมินก่อนxจากนั้น:

  • หากค่าxเป็น null yจะถูกประเมินและนั่นคือผลลัพธ์สุดท้ายของการแสดงออก
  • หากค่าของxไม่ใช่ค่าว่างyจะไม่ถูกประเมินและค่าของxเป็นผลลัพธ์สุดท้ายของการแสดงออกหลังจากแปลงเป็นประเภทการรวบรวมเวลาyหากจำเป็น

ตอนนี้มักจะมีความจำเป็นสำหรับการแปลงไม่มีหรือมันเป็นเพียงแค่จากประเภท nullable ที่ไม่ใช่ nullable หนึ่ง - ปกติชนิดที่เหมือนกันหรือเพียงแค่จาก (พูด) เพื่อint? intอย่างไรก็ตามคุณสามารถสร้างตัวดำเนินการแปลงโดยนัยของคุณเองและสิ่งเหล่านี้จะถูกใช้เมื่อจำเป็น

สำหรับกรณีง่าย ๆx ?? yฉันไม่เห็นพฤติกรรมแปลก ๆ อย่างไรก็ตามด้วยความที่(x ?? y) ?? zฉันเห็นพฤติกรรมที่สับสน

นี่คือโปรแกรมทดสอบสั้น ๆ แต่สมบูรณ์ - ผลลัพธ์อยู่ในความคิดเห็น:

using System;

public struct A
{
    public static implicit operator B(A input)
    {
        Console.WriteLine("A to B");
        return new B();
    }

    public static implicit operator C(A input)
    {
        Console.WriteLine("A to C");
        return new C();
    }
}

public struct B
{
    public static implicit operator C(B input)
    {
        Console.WriteLine("B to C");
        return new C();
    }
}

public struct C {}

class Test
{
    static void Main()
    {
        A? x = new A();
        B? y = new B();
        C? z = new C();
        C zNotNull = new C();

        Console.WriteLine("First case");
        // This prints
        // A to B
        // A to B
        // B to C
        C? first = (x ?? y) ?? z;

        Console.WriteLine("Second case");
        // This prints
        // A to B
        // B to C
        var tmp = x ?? y;
        C? second = tmp ?? z;

        Console.WriteLine("Third case");
        // This prints
        // A to B
        // B to C
        C? third = (x ?? y) ?? zNotNull;
    }
}

ดังนั้นเราจึงมีสามประเภทค่าที่กำหนดเองA, BและCมีการแปลงจาก A ไป B, A ถึง C และ B เพื่อซี

ฉันสามารถเข้าใจทั้งกรณีที่สองและกรณีที่สาม ... แต่ทำไมมีการแปลง A เป็น B พิเศษในกรณีแรก โดยเฉพาะอย่างยิ่งผมจริงๆได้คาดว่ากรณีแรกและกรณีที่สองจะเป็นสิ่งเดียวกัน - มันเป็นเพียงแค่การสกัดการแสดงออกเป็นตัวแปรท้องถิ่นหลังจากทั้งหมด

ผู้รับใด ๆ ที่เกิดขึ้น? ฉันลังเลอย่างยิ่งที่จะร้องไห้ "บั๊ก" เมื่อพูดถึงคอมไพเลอร์ C # แต่ฉันนิ่งงันว่าเกิดอะไรขึ้น ...

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

using System;

public struct A
{
    public static implicit operator int(A input)
    {
        Console.WriteLine("A to int");
        return 10;
    }
}

class Test
{
    static A? Foo()
    {
        Console.WriteLine("Foo() called");
        return new A();
    }

    static void Main()
    {
        int? y = 10;

        int? result = Foo() ?? y;
    }
}

ผลลัพธ์ของสิ่งนี้คือ:

Foo() called
Foo() called
A to int

ความจริงที่Foo()ได้รับการเรียกสองครั้งที่นี่ทำให้ฉันประหลาดใจอย่างมาก - ฉันไม่เห็นเหตุผลที่จะต้องประเมินการแสดงออกสองครั้ง


32
ฉันพนันได้เลยว่าพวกเขาคิดว่า "ไม่มีใครจะใช้มันในแบบนั้น" :)
ไซเบอร์ที่

57
ต้องการที่จะเห็นบางสิ่งบางอย่างที่เลวร้ายยิ่ง? C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);ลองใช้สายนี้กับทุกแปลงโดยปริยาย: คุณจะได้รับ:Internal Compiler Error: likely culprit is 'CODEGEN'
กำหนดค่า

5
นอกจากนี้โปรดทราบว่าสิ่งนี้จะไม่เกิดขึ้นเมื่อใช้นิพจน์ Linq เพื่อรวบรวมรหัสเดียวกัน
กำหนดค่า

8
@ รูปแบบที่ไม่น่าเป็นไปได้ แต่เป็นไปได้(("working value" ?? "user default") ?? "system default")
Factor Mystic

23
@ yes123: เมื่อมันเกี่ยวข้องกับการแปลงเพียงฉันไม่มั่นใจทั้งหมด การเห็นวิธีรันสองครั้งทำให้เห็นได้ชัดว่านี่เป็นข้อผิดพลาด คุณจะประหลาดใจกับพฤติกรรมบางอย่างที่ดูไม่ถูกต้อง แต่จริงๆแล้วถูกต้องสมบูรณ์ ทีม C # ฉลาดกว่าฉัน - ฉันมักจะคิดว่าฉันโง่จนฉันพิสูจน์ว่ามีบางอย่างผิดปกติ
Jon Skeet

คำตอบ:


418

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

ฉันยังไม่ได้ระบุว่าสิ่งใดผิดพลาดอย่างแน่นอน แต่ในบางครั้งในช่วงการรวบรวม "nullable ลด" - หลังจากการวิเคราะห์เริ่มต้น แต่ก่อนการสร้างรหัส - เราลดการแสดงออก

result = Foo() ?? y;

จากตัวอย่างด้านบนไปสู่การเทียบเท่าทางศีลธรรมของ:

A? temp = Foo();
result = temp.HasValue ? 
    new int?(A.op_implicit(Foo().Value)) : 
    y;

เห็นได้ชัดว่าไม่ถูกต้อง การลดที่ถูกต้องคือ

result = temp.HasValue ? 
    new int?(A.op_implicit(temp.Value)) : 
    y;

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

result = Foo() ?? y;

เป็นเช่นเดียวกับ

A? temp = Foo();
result = temp.HasValue ? 
    (int?) temp : 
    y;

จากนั้นเราอาจพูดเช่นนั้น

conversionResult = (int?) temp 

เป็นเช่นเดียวกับ

A? temp2 = temp;
conversionResult = temp2.HasValue ? 
    new int?(op_Implicit(temp2.Value)) : 
    (int?) null

แต่เครื่องมือเพิ่มประสิทธิภาพสามารถก้าวเข้ามาและพูดว่า "เดี๋ยวก่อนเดี๋ยวก่อนเราได้ตรวจสอบแล้วว่า temp ไม่ใช่โมฆะ; ไม่จำเป็นต้องตรวจสอบอีกครั้งสำหรับโมฆะครั้งที่สองเพียงเพราะเรากำลังเรียกผู้ดำเนินการแปลงที่ยกขึ้น" เราต้องการให้มันเพิ่มประสิทธิภาพเป็นเพียงแค่

new int?(op_Implicit(temp2.Value)) 

ฉันเดาว่าเราอยู่ที่ไหนสักแห่งแคชความจริงที่ว่ารูปแบบที่ดีที่สุดของ(int?)Foo()คือnew int?(op_implicit(Foo().Value))แต่นั่นไม่ใช่รูปแบบที่ดีที่สุดที่เราต้องการ; เราต้องการรูปแบบที่เหมาะสมของ Foo () - แทนที่ด้วยชั่วคราวและแล้วแปลง

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

เราทำการปรับโครงสร้างองค์กรใหม่ของการเขียนพาสที่ไม่สามารถเปลี่ยนค่าได้ใน C # 3.0 ข้อผิดพลาดทำซ้ำใน C # 3.0 และ 4.0 แต่ไม่ใช่ใน C # 2.0 ซึ่งหมายความว่าข้อผิดพลาดอาจเป็นของฉันไม่ดี ขออภัย!

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

อัปเดต: ฉันเขียนออพติไมเซอร์ที่ทำให้เป็นโมฆะได้ตั้งแต่ต้นสำหรับโรสลิน; มันทำงานได้ดีขึ้นและหลีกเลี่ยงข้อผิดพลาดแปลก ๆ เหล่านี้ สำหรับความคิดบางอย่างเกี่ยวกับวิธีที่เครื่องมือเพิ่มประสิทธิภาพใน Roslyn ทำงานดูบทความของฉันที่เริ่มต้นที่นี่: https://ericlippert.com/2012/12/12/nullable-micro-optimizations-part-one/


1
@Eric ฉันสงสัยว่านี่จะอธิบายได้
ไหม

12
ตอนนี้ฉันมีตัวอย่างผู้ใช้ปลายทางของ Roslyn ฉันสามารถยืนยันได้ว่ามีการแก้ไขแล้ว (มันยังคงมีอยู่ในคอมไพเลอร์พื้นเมือง C # 5)
Jon Skeet

84

นี่เป็นข้อผิดพลาดที่แน่นอนที่สุด

public class Program {
    static A? X() {
        Console.WriteLine("X()");
        return new A();
    }
    static B? Y() {
        Console.WriteLine("Y()");
        return new B();
    }
    static C? Z() {
        Console.WriteLine("Z()");
        return new C();
    }

    public static void Main() {
        C? test = (X() ?? Y()) ?? Z();
    }
}

รหัสนี้จะส่งออก:

X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)

นั่นทำให้ฉันคิดว่าส่วนแรกของแต่ละ??นิพจน์รวมกันจะถูกประเมินสองครั้ง รหัสนี้พิสูจน์แล้วว่า:

B? test= (X() ?? Y());

เอาท์พุท:

X()
X()
A to B (0)

ดูเหมือนว่าจะเกิดขึ้นเฉพาะเมื่อนิพจน์ต้องการการแปลงระหว่างสองประเภทที่สามารถ nullable ได้ ฉันลองใช้พีชคณิตต่าง ๆ โดยด้านหนึ่งเป็นสตริงและไม่มีสิ่งใดที่ทำให้เกิดพฤติกรรมนี้


11
ว้าว - การประเมินการแสดงออกสองครั้งดูเหมือนผิดมาก เห็นดี
Jon Skeet

มันง่ายกว่าเล็กน้อยในการดูว่าคุณมีการเรียกเมธอดเพียงวิธีเดียวในแหล่งที่มาหรือไม่ แต่ก็ยังแสดงให้เห็นอย่างชัดเจน
Jon Skeet

2
ฉันได้เพิ่มตัวอย่างที่ง่ายกว่าเล็กน้อยของ "การประเมินสองเท่า" ในคำถามของฉัน
Jon Skeet

8
วิธีการทั้งหมดของคุณควรที่จะแสดงผล "X ()" หรือไม่? มันทำให้ค่อนข้างยากที่จะบอกว่าวิธีการใดเป็นจริงส่งออกไปยังคอนโซล
jeffora

2
ดูเหมือนจะX() ?? Y()ขยายออกไปภายในX() != null ? X() : Y()ดังนั้นทำไมจึงต้องมีการประเมินสองครั้ง
โคลจอห์นสัน

54

หากคุณดูที่รหัสที่สร้างขึ้นสำหรับกรณีการจัดกลุ่มด้านซ้ายมันทำอะไรเช่นนี้ ( csc /optimize-):

C? first;
A? atemp = a;
B? btemp = (atemp.HasValue ? new B?(a.Value) : b);
if (btemp.HasValue)
{
    first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value);
}

พบอีกถ้าคุณใช้ firstมันจะสร้างทางลัดถ้าทั้งสองaและเป็นโมฆะและผลตอบแทนb cแต่ถ้าaหรือbไม่เป็นโมฆะมันประเมินอีกครั้งaเป็นส่วนหนึ่งของการแปลงโดยปริยายไปBก่อนที่จะส่งคืนaหรือbไม่เป็นโมฆะ

จากข้อกำหนด C # 4.0, §6.1.4:

  • หากการแปลงเป็นโมฆะจากS?ถึงT?:
    • ถ้าค่าแหล่งที่มาnull( HasValueทรัพย์สินfalse) ผลที่ได้คือค่าของชนิดnullT?
    • มิฉะนั้นการแปลงได้รับการประเมินเป็นแกะจากS?ที่จะSตามมาด้วยการแปลงต้นแบบจากSที่จะTตามมาด้วยการตัด (§4.1.10) จากการTT?

สิ่งนี้ดูเหมือนจะอธิบายชุดที่ห่อหุ้มห่อที่สอง


คอมไพเลอร์ C # 2008 และ 2010 สร้างโค้ดที่คล้ายกันมาก แต่นี่ดูเหมือนว่าการถดถอยจากคอมไพเลอร์ C # 2005 (8.00.50727.4927) ซึ่งสร้างโค้ดต่อไปนี้สำหรับด้านบน:

A? a = x;
B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y;
C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z;

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


+1 แต่ฉันไม่คิดว่ามันจะอธิบายได้ว่าทำไมการแปลงจึงทำได้สองครั้ง มันควรจะประเมินการแสดงออกเพียงครั้งเดียว IMO
Jon Skeet

@ จอน: ฉันเล่นไปเรื่อย ๆ แล้วพบ (ตามที่ @configurator ทำ) ว่าเมื่อทำใน Expression Tree แล้วจะทำงานได้ตามที่คาดไว้ กำลังพยายามล้างข้อมูลนิพจน์เพื่อเพิ่มลงในโพสต์ของฉัน ฉันจะต้องโพสต์แล้วว่านี่คือ "ข้อผิดพลาด"
user7116

@ จอน: ตกลงเมื่อใช้ Expression Trees มันจะกลาย(x ?? y) ?? zเป็น lambdas ที่ซ้อนกันซึ่งจะทำให้การประเมินตามลำดับโดยไม่ต้องทำการประเมินซ้ำสองครั้ง เห็นได้ชัดว่านี่ไม่ใช่วิธีการที่คอมไพเลอร์ C # 4.0 จากสิ่งที่ฉันสามารถบอกได้ส่วน 6.1.4 ได้รับการติดต่ออย่างเข้มงวดมากในเส้นทางรหัสนี้โดยเฉพาะและไม่ได้มีการนำกาลเวลามาใช้ในการประเมินซ้ำ
user7116

16

จริงๆแล้วฉันจะเรียกสิ่งนี้ว่าข้อผิดพลาดตอนนี้พร้อมตัวอย่างที่ชัดเจนกว่า สิ่งนี้ยังคงอยู่ แต่การประเมินสองครั้งไม่ดีอย่างแน่นอน

มันดูเหมือนกับว่าจะดำเนินการเป็นA ?? B A.HasValue ? A : Bในกรณีนี้มีจำนวนมากของการหล่อเกินไป (ต่อไปนี้หล่อปกติสำหรับ ternary ?:ดำเนินการ) แต่ถ้าคุณไม่สนใจสิ่งนั้นสิ่งนี้ก็สมเหตุสมผลตามการใช้งาน:

  1. A ?? B ขยายเป็น A.HasValue ? A : B
  2. Ax ?? yเป็นของเรา ขยายไปยังx.HasValue : x ? y
  3. แทนที่ A -> ที่เกิดขึ้นทั้งหมด (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

ที่นี่คุณจะเห็นว่าx.HasValueมีการตรวจสอบสองครั้งและหากx ?? yต้องการการคัดเลือกนักแสดงxจะถูกคัดเลือกสองครั้ง

ฉันจะวางมันลงอย่างง่ายๆเป็นสิ่งประดิษฐ์ของวิธี??การใช้งานมากกว่าข้อผิดพลาดของคอมไพเลอร์ Take-Away: อย่าสร้างตัวดำเนินการแคสต์โดยนัยด้วยผลข้างเคียง

ดูเหมือนว่าจะเป็นข้อผิดพลาดของคอมไพเลอร์ซึ่งหมุนรอบวิธี??การใช้งาน Take-away: อย่าซ้อนการรวมกลุ่มของนิพจน์ด้วยผลข้างเคียง


โอ้แน่นอนฉันจะไม่ต้องการที่จะใช้รหัสเช่นนี้ตามปกติ แต่ผมคิดว่ามันอาจจะยังคงถูกจัดว่าเป็นข้อผิดพลาดของคอมไพเลอร์ในการที่ขยายตัวครั้งแรกของคุณควรรวมถึง "แต่เพียงการประเมิน A และ B ครั้งเดียว" (ลองคิดดูว่าพวกเขาใช้วิธีการแบบนั้นหรือเปล่า)
Jon Skeet

@ จอนฉันยอมรับว่ามันอาจจะเป็นเช่นกัน - แต่ฉันจะไม่เรียกมันว่าชัดเจน ที่จริงฉันเห็นแล้วว่าA() ? A() : B()อาจมีการประเมินA()สองครั้ง แต่A() ?? B()ไม่มาก และเนื่องจากมันเกิดขึ้นกับการร่าย ... อืม .. ฉันแค่พูดกับตัวเองว่าคิดว่ามันไม่ได้ทำงานอย่างถูกต้อง
ฟิลิป Rieck

10

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

ฉันได้มาถึงbugข้อสรุปนี้ด้วยการสร้างโปรแกรมรุ่นที่แตกต่างซึ่งเกี่ยวข้องกับสถานการณ์เดียวกัน แต่มีความซับซ้อนน้อยกว่ามาก

ฉันใช้คุณสมบัติเลขจำนวนเต็มสามค่าด้วยร้านค้าสำรอง ฉันตั้งค่าเป็น 4 แล้วเรียกใช้int? something2 = (A ?? B) ?? C;

( รหัสเต็มนี่ )

แค่อ่าน A และไม่มีอะไรอื่น

ข้อความนี้ถึงฉันดูเหมือนว่าฉันควรจะ:

  1. เริ่มต้นในวงเล็บเหลี่ยมดูที่ A คืนค่า A และเสร็จสิ้นหาก A ไม่เป็นโมฆะ
  2. ถ้า A เป็นโมฆะให้ประเมิน B ให้เสร็จสิ้นหาก B ไม่ใช่โมฆะ
  3. ถ้า A และ B เป็นโมฆะให้ประเมิน C

ดังนั้นเนื่องจาก A ไม่ใช่โมฆะมันจึงดูที่ A และเสร็จสิ้นเท่านั้น

ในตัวอย่างของคุณการใส่จุดพักที่กรณีแรกแสดงให้เห็นว่า x, y และ z นั้นไม่เป็นโมฆะดังนั้นฉันคาดว่าพวกเขาจะได้รับการปฏิบัติเช่นเดียวกับตัวอย่างที่ซับซ้อนน้อยกว่าของฉัน .... แต่ฉันกลัวว่าฉันมากเกินไป จาก newbie C # และพลาดจุดคำถามนี้ไปอย่างสิ้นเชิง!


5
ตัวอย่างของจอนเป็นกรณีมุมที่คลุมเครือในกรณีที่เขาใช้ struct ที่ไม่มีค่าintได้ เขาผลักคดีไปสู่มุมที่ไม่ชัดเจนโดยจัดให้มีการแปลงหลายประเภทโดยนัย นี้ต้องใช้คอมไพเลอร์ในการเปลี่ยนแปลงประเภทnullของข้อมูลในขณะที่การตรวจสอบกับ เป็นเพราะการแปลงประเภทโดยนัยเหล่านี้ทำให้ตัวอย่างของเขาแตกต่างจากของคุณ
user7116
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.