C # โอเคกับการเปรียบเทียบประเภทค่าเป็นโมฆะ


85

ฉันพบสิ่งนี้ในวันนี้และไม่รู้ว่าทำไมคอมไพเลอร์ C # ไม่แสดงข้อผิดพลาด

Int32 x = 1;
if (x == null)
{
    Console.WriteLine("What the?");
}

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

Int32 x = null;

เป็นไปได้ไหมที่ x อาจกลายเป็นโมฆะ Microsoft เพิ่งตัดสินใจที่จะไม่ใส่เช็คนี้ลงในคอมไพเลอร์หรือว่าพลาดไปโดยสิ้นเชิง?

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

public class Test
{
    public DateTime ADate = DateTime.Now;

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}

9
คุณสามารถเขียนได้if (1 == 2)เช่นกัน ไม่ใช่หน้าที่ของคอมไพเลอร์ในการวิเคราะห์เส้นทางรหัส นั่นคือเครื่องมือวิเคราะห์แบบคงที่และการทดสอบหน่วย
Aaronaught

เหตุใดคำเตือนจึงหายไปโปรดดูคำตอบของฉัน และไม่ - มันไม่สามารถเป็นโมฆะได้
Marc Gravell

1
เห็นด้วยกับ (1 == 2) ฉันสงสัยมากขึ้นเกี่ยวกับสถานการณ์ (1 == null)
Joshua Belden

ขอบคุณทุกคนที่ตอบกลับ ทั้งหมดนี้สมเหตุสมผลแล้ว
Joshua Belden

เกี่ยวกับปัญหาการเตือนหรือไม่มีคำเตือน: หากโครงสร้างที่เป็นปัญหาเป็นสิ่งที่เรียกว่า "ประเภทธรรมดา" เช่นเดียวintกับคอมไพเลอร์จะสร้างคำเตือนที่ดี สำหรับประเภทอย่างง่ายตัว==ดำเนินการถูกกำหนดโดยข้อกำหนดภาษา C # สำหรับโครงสร้างอื่น ๆ (ไม่ใช่แบบธรรมดา) คอมไพเลอร์ลืมที่จะส่งเสียงเตือน ดูคำเตือนคอมไพเลอร์ที่ไม่ถูกต้องเมื่อเปรียบเทียบโครงสร้างกับ nullสำหรับรายละเอียด สำหรับโครงสร้างที่ไม่ใช่ประเภทธรรมดาตัว==ดำเนินการจะต้องโอเวอร์โหลดโดยopeartor ==เมธอดซึ่งเป็นสมาชิกของโครงสร้าง (มิฉะนั้น==จะไม่ได้รับอนุญาต)
Jeppe Stig Nielsen

คำตอบ:


119

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

ในทำนองเดียวกันเรายังอนุญาตให้คุณพูดว่า "if (x == 12.6)" ซึ่งจะเป็นเท็จเสมอ int local สามารถแปลงเป็นสองเท่าลิเทอรัลสามารถแปลงเป็นสองเท่าได้และเห็นได้ชัดว่าพวกมันจะไม่เท่ากัน


4
แสดงความคิดเห็นของคุณ: connect.microsoft.com/VisualStudio/feedback/…
Marc Gravell

5
@ เจมส์: (ฉันถอนกลับความคิดเห็นที่ผิดพลาดก่อนหน้านี้ซึ่งฉันได้ลบไปแล้ว) ประเภทค่าที่ผู้ใช้กำหนดซึ่งมีตัวดำเนินการความเท่าเทียมกันที่กำหนดโดยผู้ใช้ซึ่งกำหนดโดยค่าเริ่มต้นจะมีตัวดำเนินการความเท่าเทียมกันที่ผู้ใช้กำหนดขึ้นซึ่งสร้างขึ้นสำหรับพวกเขา ตัวดำเนินการความเท่าเทียมกันที่กำหนดโดยผู้ใช้ที่ยกขึ้นสามารถใช้ได้กับเหตุผลที่คุณระบุ: ประเภทค่าทั้งหมดสามารถแปลงได้โดยปริยายเป็นประเภท nullable ที่สอดคล้องกันเช่นเดียวกับลิเทอรัล null มันเป็นไม่ได้เป็นกรณีที่ผู้ใช้กำหนดประเภทค่าที่ขาดผู้ประกอบการเปรียบเทียบที่ผู้ใช้กำหนดก็เปรียบได้กับตัวอักษรเป็นโมฆะ
Eric Lippert

3
@ เจมส์: แน่นอนว่าคุณสามารถใช้ตัวดำเนินการ == และตัวดำเนินการของคุณเอง! = ที่ใช้โครงสร้างว่างได้ หากมีอยู่คอมไพเลอร์จะใช้แทนที่จะสร้างให้คุณโดยอัตโนมัติ (และโดยบังเอิญฉันเสียใจที่คำเตือนสำหรับตัวดำเนินการยกที่ไม่มีความหมายบนตัวถูกดำเนินการที่ไม่เป็นโมฆะไม่ได้สร้างคำเตือนนั่นเป็นข้อผิดพลาดในคอมไพเลอร์ที่เราไม่สามารถแก้ไขได้)
Eric Lippert

2
เราต้องการคำเตือนของเรา! เราสมควรได้รับมัน
Jeppe Stig Nielsen

3
@JamesDunne: แล้วการกำหนด a static bool operator == (SomeID a, String b)และติดแท็กด้วยObsoleteล่ะ? หากตัวถูกดำเนินการตัวที่สองเป็นลิเทอรัnullลที่ไม่ได้พิมพ์นั่นจะเป็นการจับคู่ที่ดีกว่ารูปแบบใด ๆ ที่ต้องใช้ตัวดำเนินการแบบยก แต่ถ้าตัวดำเนินการSomeID?ที่มีค่าเท่ากันnullตัวดำเนินการยกจะชนะ
supercat

17

ไม่ใช่ข้อผิดพลาดเนื่องจากมีการint?แปลง( ) มันสร้างคำเตือนในตัวอย่างที่กำหนด:

ผลลัพธ์ของนิพจน์จะเป็น 'เท็จ' เสมอเนื่องจากค่าของประเภท 'int' จะไม่เท่ากับ 'null' ของประเภท 'int?'

หากคุณตรวจสอบ IL คุณจะเห็นว่ามันลบสาขาที่ไม่สามารถเข้าถึงได้อย่างสมบูรณ์ซึ่งไม่มีอยู่ในรุ่นรุ่น

อย่างไรก็ตามโปรดทราบว่าจะไม่สร้างคำเตือนสำหรับโครงสร้างแบบกำหนดเองที่มีตัวดำเนินการความเท่าเทียมกัน เคยเป็นใน 2.0 แต่ไม่ใช่ในคอมไพเลอร์ 3.0 รหัสยังคงถูกลบออก (ดังนั้นจึงรู้ว่ารหัสไม่สามารถเข้าถึงได้) แต่ไม่มีการสร้างคำเตือน:

using System;

struct MyValue
{
    private readonly int value;
    public MyValue(int value) { this.value = value; }
    public static bool operator ==(MyValue x, MyValue y) {
        return x.value == y.value;
    }
    public static bool operator !=(MyValue x, MyValue y) {
        return x.value != y.value;
    }
}
class Program
{
    static void Main()
    {
        int i = 1;
        MyValue v = new MyValue(1);
        if (i == null) { Console.WriteLine("a"); } // warning
        if (v == null) { Console.WriteLine("a"); } // no warning
    }
}

ด้วย IL (สำหรับMain) - บันทึกทุกอย่างยกเว้นMyValue(1)(ซึ่งอาจมีผลข้างเคียง) ถูกลบ:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] valuetype MyValue v)
    L_0000: ldc.i4.1 
    L_0001: stloc.0 
    L_0002: ldloca.s v
    L_0004: ldc.i4.1 
    L_0005: call instance void MyValue::.ctor(int32)
    L_000a: ret 
}

โดยพื้นฐานแล้ว:

private static void Main()
{
    MyValue v = new MyValue(1);
}

1
เมื่อเร็ว ๆ นี้มีคนรายงานเรื่องนี้ให้ฉันทราบเป็นการภายใน ฉันไม่รู้ว่าทำไมเราถึงหยุดผลิตคำเตือนนั้น เราได้ป้อนเป็นบั๊ก
Eric Lippert


5

การที่การเปรียบเทียบไม่สามารถเป็นจริงได้ไม่ได้หมายความว่ามันผิดกฎหมาย อย่างไรก็ตามไม่มีประเภทค่าสามารถเป็นnullได้


1
แต่ชนิดของมูลค่าอาจจะเท่ากับnullไป พิจารณาint?ซึ่งเป็นน้ำตาลในรูปแบบNullable<Int32>ที่เป็นค่านิยม ตัวแปรประเภทแน่นอนอาจจะเท่ากับint? null
Greg

1
@Greg: ใช่มันสามารถเท่ากับ null ได้โดยสมมติว่า "เท่ากับ" ที่คุณอ้างถึงเป็นผลลัพธ์ของตัว==ดำเนินการ สิ่งสำคัญคือต้องทราบว่าอินสแตนซ์ไม่ได้เป็นโมฆะจริงๆ
Adam Robinson

3

ไม่มีจะไม่เคยเป็น Int32 xnull

หากคุณกำลังเปรียบเทียบ int กับ null ตัวดำเนินการเปรียบเทียบที่ใช้สอง int? s จะใช้ได้

"เหตุใดการเปรียบเทียบประเภทค่ากับ null จึงเป็นคำเตือน" บทความจะช่วยคุณ


1

ประเภทค่าไม่สามารถเป็นได้nullแม้ว่าจะเท่ากับnull(พิจารณาNullable<>) ในกรณีของคุณintตัวแปรและnullถูกโยนโดยปริยายNullable<Int32>และเปรียบเทียบ


0

ฉันสงสัยว่าการทดสอบเฉพาะของคุณเพิ่งได้รับการปรับให้เหมาะสมโดยคอมไพเลอร์เมื่อสร้าง IL เนื่องจากการทดสอบจะไม่เป็นเท็จ

หมายเหตุด้านข้าง: เป็นไปได้ที่จะมี Int32 ที่เป็นโมฆะใช้ Int32? x แทน


0

ฉันเดาว่านี่เป็นเพราะ "==" เป็นไวยากรณ์น้ำตาลซึ่งแสดงถึงSystem.Object.Equalsวิธีการเรียกใช้ที่ยอมรับSystem.Objectพารามิเตอร์ Null by ECMA specification เป็นชนิดพิเศษซึ่งแน่นอนมาจากSystem.Objectเป็นชนิดพิเศษซึ่งเป็นหลักสูตรที่ได้มาจาก

นั่นเป็นเหตุผลว่าทำไมจึงมีเพียงคำเตือน


สิ่งนี้ไม่ถูกต้องด้วยเหตุผลสองประการ อันดับแรก == ไม่มีความหมายเดียวกันกับ Object เท่ากันเมื่ออาร์กิวเมนต์ใดอาร์กิวเมนต์เป็นประเภทอ้างอิง ประการที่สอง null ไม่ใช่ประเภท ดูส่วน 7.9.6 ของข้อกำหนดหากคุณต้องการทำความเข้าใจว่าตัวดำเนินการความเท่าเทียมกันอ้างอิงทำงานอย่างไร
Eric Lippert

"ค่า null ลิเทอรัล (§9.4.4.6) ประเมินเป็นค่า null ซึ่งใช้เพื่อแสดงการอ้างอิงที่ไม่ชี้ไปที่วัตถุหรืออาร์เรย์ใด ๆ หรือไม่มีค่าประเภท null มีค่าเดียวซึ่งเป็นค่าว่าง ค่าดังนั้นนิพจน์ที่มีประเภทเป็นประเภท null สามารถประเมินได้เฉพาะกับค่า null ไม่มีวิธีใดที่จะเขียนประเภท null อย่างชัดเจนดังนั้นจึงไม่มีวิธีใช้ในประเภทที่ประกาศ " - นี่คือคำพูดจาก ECMA คุณกำลังพูดถึงอะไร? คุณใช้ ECMA เวอร์ชันใด ฉันไม่เห็น 7.9.6 ในของฉัน
Vitaly

0

[แก้ไข: ทำให้คำเตือนเป็นข้อผิดพลาดและทำให้ตัวดำเนินการมีความชัดเจนเกี่ยวกับ nullable แทนที่จะเป็นสตริงแฮ็ค]

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

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

จนกว่า Microsoft จะเตือนเรากลับคอมไพเลอร์ของเราฉันจะใช้วิธีแก้ปัญหานี้ขอบคุณ @supercat!

public struct Foo
{
    private readonly int x;
    public Foo(int x)
    {
        this.x = x;
    }

    public override string ToString()
    {
        return string.Format("Foo {{x={0}}}", x);
    }

    public override int GetHashCode()
    {
        return x.GetHashCode();
    }

    public override bool Equals(Object obj)
    {
        return x.Equals(obj);
    }

    public static bool operator ==(Foo a, Foo b)
    {
        return a.x == b.x;
    }

    public static bool operator !=(Foo a, Foo b)
    {
        return a.x != b.x;
    }

    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo a, Foo? b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo a, Foo? b)
    {
        return true;
    }
    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo? a, Foo b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo? a, Foo b)
    {
        return true;
    }
}

วิธีการของคุณจะทำให้คอมไพเลอร์Foo a; Foo? b; ... if (a == b)...ไม่พอใจแม้ว่าการเปรียบเทียบดังกล่าวควรถูกต้องตามกฎหมายอย่างสมบูรณ์ เหตุผลที่ฉันปัญหาสตริง "สับ" ก็คือว่ามันจะช่วยให้เปรียบเทียบข้างต้น if (a == null)แต่ที่เต้นแร้งเต้นกา แทนการใช้stringหนึ่งสามารถทดแทนชนิดการอ้างอิงใด ๆ นอกเหนือObjectหรือValueType; ReferenceThatCanOnlyBeNullหากต้องการใครสามารถกำหนดระดับหุ่นกับนวกรรมิกเอกชนที่อาจจะไม่เคยถูกเรียกและสิทธิมัน
supercat

คุณถูกต้องที่สุด ฉันควรจะชี้แจงว่าข้อเสนอแนะของฉันทำลายการใช้ nullables ... ;)
โยโย่

0

ฉันคิดว่าคำตอบที่ดีที่สุดว่าทำไมคอมไพเลอร์ยอมรับสิ่งนี้สำหรับคลาสทั่วไป พิจารณาชั้นเรียนต่อไปนี้ ...

public class NullTester<T>
{
    public bool IsNull(T value)
    {
        return (value == null);
    }
}

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


0

คอมไพเลอร์จะช่วยให้คุณสามารถเปรียบเทียบโครงสร้างใด ๆ ที่ใช้ ==กับ null มันยังช่วยให้คุณสามารถเปรียบเทียบ int กับ null ได้ (คุณจะได้รับคำเตือน)

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

public static void Main()
{
    Console.WriteLine(new Foo() == new Foo());
    Console.WriteLine(new Foo() == null);
    Console.WriteLine(5 == null);
    Console.WriteLine(new Foo() != null);
}

สร้าง IL นี้:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  2
  .locals init ([0] valuetype test3.Program/Foo V_0)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    test3.Program/Foo
  IL_0009:  ldloc.0
  IL_000a:  ldloca.s   V_0
  IL_000c:  initobj    test3.Program/Foo
  IL_0012:  ldloc.0
  IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                           valuetype test3.Program/Foo)
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_001d:  nop
  IL_001e:  ldc.i4.0
  IL_001f:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0024:  nop
  IL_0025:  ldc.i4.1
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

อย่างที่เห็น:

Console.WriteLine(new Foo() == new Foo());

แปลเป็น:

IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                               valuetype test3.Program/Foo)

ในขณะที่:

Console.WriteLine(new Foo() == null);

แปลเป็นเท็จ:

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