ทุกคนสามารถอธิบายพฤติกรรมที่แปลกประหลาดนี้ด้วยการเซ็นชื่อลอยใน C #?


247

นี่คือตัวอย่างที่มีความคิดเห็น:

class Program
{
    // first version of structure
    public struct D1
    {
        public double d;
        public int f;
    }

    // during some changes in code then we got D2 from D1
    // Field f type became double while it was int before
    public struct D2 
    {
        public double d;
        public double f;
    }

    static void Main(string[] args)
    {
        // Scenario with the first version
        D1 a = new D1();
        D1 b = new D1();
        a.f = b.f = 1;
        a.d = 0.0;
        b.d = -0.0;
        bool r1 = a.Equals(b); // gives true, all is ok

        // The same scenario with the new one
        D2 c = new D2();
        D2 d = new D2();
        c.f = d.f = 1;
        c.d = 0.0;
        d.d = -0.0;
        bool r2 = c.Equals(d); // false! this is not the expected result        
    }
}

แล้วคุณคิดอย่างไรเกี่ยวกับเรื่องนี้?


2
เพื่อทำให้สิ่งที่คนแปลกหน้าc.d.Equals(d.d)ประเมินtrueเช่นเดียวกับมันc.f.Equals(d.f)
Justin Niessner

2
อย่าเปรียบเทียบการลอยด้วยการเปรียบเทียบที่แม่นยำเช่น เป็นเพียงความคิดที่ไม่ดี
Thorsten79

6
@ Thorsten79: มีความเกี่ยวข้องที่นี่ได้อย่างไร
Ben M

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

1
แปลก - มันดูเหมือนว่าจะเกิดขึ้นเมื่อทั้งสองเป็นประเภทเดียวกัน (ลอยหรือสองครั้ง) เปลี่ยนหนึ่งเป็นทศนิยม (หรือทศนิยม) และ D2 ทำงานเหมือนกับ D1
tvanfosson

คำตอบ:


387

ข้อผิดพลาดอยู่ในสองบรรทัดต่อไปนี้System.ValueType: (ฉันก้าวเข้าสู่แหล่งอ้างอิง)

if (CanCompareBits(this)) 
    return FastEqualsCheck(thisObj, obj);

(ทั้งสองวิธี[MethodImpl(MethodImplOptions.InternalCall)])

เมื่อเขตข้อมูลทั้งหมดมีขนาด 8 ไบต์ความCanCompareBitsผิดพลาดจะส่งกลับค่าจริงโดยไม่ตั้งใจทำให้มีการเปรียบเทียบค่าที่ต่างกันสองค่า แต่มีความเหมือนกันทางความหมาย

เมื่อข้อมูลอย่างน้อยหนึ่งไม่ได้เป็น 8 ไบต์กว้างCanCompareBitsผลตอบแทนเท็จและเงินโค้ดเพื่อสะท้อนการใช้ห่วงมากกว่าทุ่งนาและโทรEqualsสำหรับแต่ละค่าที่ถูกต้องถือว่าเป็นไปได้เท่ากัน-0.00.0

นี่คือแหล่งที่มาของCanCompareBitsจาก SSCLI:

FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
    WRAPPER_CONTRACT;
    STATIC_CONTRACT_SO_TOLERANT;

    _ASSERTE(obj != NULL);
    MethodTable* mt = obj->GetMethodTable();
    FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND

159
ก้าวเข้าสู่ System.ValueType? นั่นเป็นฮาร์ดคอร์ไม่ยอมใครง่ายๆ
Pierreten

2
คุณไม่ได้อธิบายความสำคัญของ "ความกว้าง 8 ไบต์" struct ที่มีเขตข้อมูลขนาด 4 ไบต์ทั้งหมดจะไม่มีผลลัพธ์เหมือนกันหรือไม่ ฉันเดาว่าการมีเขตข้อมูล 4 ไบต์เดียวและเขตข้อมูล 8 ไบต์เป็นเพียงIsNotTightlyPackedจุดเริ่มต้น
Gabe

1
@Gabe ฉันเขียนไว้ก่อนหน้านี้ว่าThe bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks

1
ด้วย .NET เป็นซอฟต์แวร์โอเพ่นซอร์สตอนนี้ที่นี่คือการเชื่อมโยงการดำเนินงานหลักของ CLR ValueTypeHelper :: CanCompareBits ไม่ต้องการอัปเดตคำตอบของคุณเนื่องจากการใช้งานจะเปลี่ยนไปเล็กน้อยจากแหล่งข้อมูลอ้างอิงที่คุณโพสต์
IIsspectable

59

ผมพบคำตอบที่http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx

ชิ้นส่วนหลักคือความคิดเห็นของแหล่งที่มาCanCompareBitsซึ่งValueType.Equalsใช้ในการพิจารณาว่าจะใช้memcmpการเปรียบเทียบสไตล์หรือไม่:

ความคิดเห็นของ CanCompareBits พูดว่า "Return true หากค่าประเภทไม่มีตัวชี้และบรรจุแน่น" และ FastEqualsCheck ใช้ "memcmp" เพื่อเร่งการเปรียบเทียบ

ผู้เขียนกล่าวต่อไปว่าปัญหาที่อธิบายโดย OP:

ลองนึกภาพคุณมีโครงสร้างที่มีเพียงลอย จะเกิดอะไรขึ้นถ้ามี +0.0 และอีกอันมี -0.0 พวกเขาควรจะเหมือนกัน แต่การเป็นตัวแทนไบนารีพื้นฐานที่แตกต่างกัน หากคุณซ้อนโครงสร้างอื่น ๆ ซึ่งแทนที่เมธอด Equals การปรับให้เหมาะสมนั้นจะล้มเหลวเช่นกัน


ฉันสงสัยว่าพฤติกรรมของEquals(Object)สำหรับdouble, floatและDecimalการเปลี่ยนแปลงในช่วงเริ่มต้นของการร่าง .net; ฉันคิดว่ามันเป็นสิ่งสำคัญมากที่จะมีเสมือนX.Equals((Object)Y)การกลับมาเท่านั้นtrueเมื่อXและYจะแยกไม่ออกมากกว่าที่จะมีวิธีการที่ตรงกับพฤติกรรมของ overloads อื่น ๆ (โดยเฉพาะอย่างยิ่งที่ว่าเนื่องจากประเภทบังคับโดยปริยายมากเกินไปEqualsวิธีการไม่ได้กำหนดความสมดุล !, เช่น1.0f.Equals(1.0)ให้ผลเป็นเท็จ, แต่1.0.Equals(1.0f)ให้ผลเป็นจริง!) ปัญหาที่แท้จริง IMHO ไม่ได้อยู่ที่วิธีเปรียบเทียบโครงสร้าง ...
supercat

1
... แต่ด้วยวิธีที่ประเภทค่าเหล่านั้นEqualsแทนที่จะหมายถึงสิ่งอื่นที่เทียบเท่า ตัวอย่างเช่นสมมติว่าเราต้องการเขียนวิธีการที่ใช้วัตถุที่ไม่เปลี่ยนรูปและถ้ามันยังไม่ถูกแคชให้ดำเนินการToStringกับมันและแคชผลลัพธ์ ถ้ามันถูกแคชเพียงแค่ส่งกลับสตริงแคช ไม่ใช่สิ่งที่ไม่มีเหตุผลที่ต้องทำ แต่มันจะล้มเหลวอย่างรุนแรงDecimalเนื่องจากค่าสองค่าอาจเปรียบเทียบเท่ากัน แต่ให้ผลสตริงที่แตกต่างกัน
supercat

52

การคาดเดาของ Vilx ถูกต้อง สิ่งที่ "CanCompareBits" ทำคือการตรวจสอบเพื่อดูว่าประเภทของค่าที่เป็นปัญหานั้นถูก "บรรจุแน่น" ในหน่วยความจำหรือไม่ มีการเปรียบเทียบโครงสร้างที่มีการบีบอัดแน่นโดยการเปรียบเทียบไบนารีบิตที่ประกอบกันเป็นโครงสร้าง มีการเปรียบเทียบโครงสร้างที่แน่นหนาโดยการโทรเท่ากับสมาชิกทั้งหมด

สิ่งนี้อธิบายการสังเกตของ SLaks ว่ามัน repros ด้วย structs ที่เป็นสองเท่าทั้งหมด structs ดังกล่าวจะถูกบรรจุอย่างแน่นหนาเสมอ

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


3
ถ้าอย่างนั้นทำไมมันไม่ใช่บั๊ก? แม้ว่า MS แนะนำให้แทนที่ Equals กับชนิดของค่าเสมอ
Alexander Efimov

14
เอาชนะความหายนะจากฉัน ฉันไม่ใช่ผู้เชี่ยวชาญเกี่ยวกับเรื่องภายในของ CLR
Eric Lippert

4
... คุณไม่ แน่นอนว่าความรู้ของคุณเกี่ยวกับ C # internals จะนำไปสู่ความรู้อย่างมากเกี่ยวกับการทำงานของ CLR
CaptainCasey

37
@CaptainCasey: ฉันใช้เวลาห้าปีในการศึกษา internals ของคอมไพเลอร์ C # และอาจใช้เวลาสองสามชั่วโมงในการศึกษา internals ของ CLR จำไว้ว่าฉันเป็นผู้บริโภคของ CLR; ฉันเข้าใจพื้นที่ผิวสาธารณะของมันค่อนข้างดี แต่ภายในเป็นกล่องดำสำหรับฉัน
Eric Lippert

1
ความผิดพลาดของฉันฉันคิดว่า CLR และคอมไพเลอร์ VB / C # นั้นเชื่อมโยงกันอย่างแน่นหนา ... ดังนั้น C # / VB -> CIL -> CLR
CaptainCasey

22

ครึ่งคำตอบ:

Reflector บอกเราว่าValueType.Equals()ทำสิ่งนี้:

if (CanCompareBits(this))
    return FastEqualsCheck(this, obj);
else
    // Use reflection to step through each member and call .Equals() on each one.

น่าเสียดายที่ทั้งสองCanCompareBits()และFastEquals()(ทั้งวิธีคงที่) เป็น extern ( [MethodImpl(MethodImplOptions.InternalCall)]) และไม่มีแหล่งที่มา

กลับไปที่การคาดเดาว่าทำไมกรณีหนึ่งสามารถเปรียบเทียบโดยบิตและอีกกรณีหนึ่งไม่สามารถ (อาจมีปัญหาการจัดตำแหน่ง)


17

มันไม่ให้เป็นจริงสำหรับฉันกับโมโน GMCs 2.4.2.3


5
ใช่ฉันได้ลองใช้งานใน Mono แล้วและมันก็ทำให้ฉันเป็นจริงเช่นกัน ดูเหมือนว่า MS จะใช้เวทย์มนตร์ข้างใน :)
อเล็กซานเดอร์เอฟโมอฟ

3
น่าสนใจเราจัดส่งไปที่โมโนทั้งหมดหรือไม่
WeNeedAnswers

14

กรณีทดสอบที่ง่ายขึ้น:

Console.WriteLine("Good: " + new Good().Equals(new Good { d = -.0 }));
Console.WriteLine("Bad: " + new Bad().Equals(new Bad { d = -.0 }));

public struct Good {
    public double d;
    public int f;
}

public struct Bad {
    public double d;
}

แก้ไข : ข้อผิดพลาดยังเกิดขึ้นกับลอย แต่จะเกิดขึ้นเฉพาะเมื่อเขตข้อมูลใน struct เพิ่มขึ้นหลายคูณ 8 ไบต์


ดูเหมือนว่ากฎของเครื่องมือเพิ่มประสิทธิภาพจะดำเนินต่อไป: หากทั้งหมดมีการเปรียบเทียบเป็นสองเท่าเปรียบเทียบกันจะแยกกันสองครั้งการโทรที่มีประสิทธิภาพ
Henk Holterman

ฉันไม่คิดว่านี่เป็นกรณีทดสอบเดียวกับปัญหาที่ปรากฏในที่นี้คือค่าเริ่มต้นสำหรับ Bad.f ไม่ใช่ 0 ในขณะที่กรณีอื่น ๆ น่าจะเป็นปัญหา Int vs. Double
Driss Zouak

6
@Driss: ค่าเริ่มต้นสำหรับมีdouble 0คุณผิด.
Slaks

10

จะต้องเกี่ยวข้องกับการเปรียบเทียบแบบบิตต่อบิตเนื่องจาก0.0ควรแตกต่างจาก-0.0สัญญาณบิตเท่านั้น


5

…คุณคิดอย่างไรเกี่ยวกับเรื่องนี้?

แทนที่ Equals และ GetHashCode เสมอในประเภทของค่า มันจะรวดเร็วและถูกต้อง


นอกเหนือจากข้อแม้ที่ว่านี้เป็นสิ่งจำเป็นเฉพาะเมื่อความเท่าเทียมกันมีความเกี่ยวข้องนี่คือสิ่งที่ฉันคิด สนุกเหมือนที่ได้ดูพฤติกรรมแปลกๆ ของพฤติกรรมความเสมอภาคประเภทค่าเริ่มต้นเช่นคำตอบที่ได้รับคะแนนสูงสุดมีเหตุผลว่าทำไมCA1815จึงมีอยู่
Joe Amenta

@JoeAmenta ขออภัยสำหรับคำตอบที่ล่าช้า ในมุมมองของฉัน (ในมุมมองของฉันแน่นอน) ความเสมอภาคเสมอ ( ) ที่เกี่ยวข้องกับประเภทของค่า การใช้งานความเท่าเทียมกันเริ่มต้นไม่เป็นที่ยอมรับในกรณีทั่วไป ( ) ยกเว้นกรณีพิเศษมาก มาก. พิเศษมาก. เมื่อคุณรู้ว่าคุณกำลังทำอะไรและทำไม
Viacheslav Ivanov

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

4

เพียงอัปเดตสำหรับข้อบกพร่องอายุ 10 ปีนี้: ได้รับการแก้ไขแล้ว ( ข้อจำกัดความรับผิดชอบ : ฉันเป็นผู้เขียนข่าวประชาสัมพันธ์นี้) ใน. NET Core ซึ่งอาจมีการเปิดตัวใน. NET Core 2.1.0

บล็อกโพสต์อธิบายข้อผิดพลาดและวิธีการที่ฉันคงมัน


2

ถ้าคุณทำ D2 เช่นนี้

public struct D2
{
    public double d;
    public double f;
    public string s;
}

มันเป็นความจริง.

ถ้าคุณทำเช่นนี้

public struct D2
{
    public double d;
    public double f;
    public double u;
}

มันยังคงเป็นเท็จ

ฉันดูเหมือนว่ามันจะผิดพลาดถ้า struct ถือเพียงสองเท่า


1

มันจะต้องเกี่ยวข้องกับศูนย์ตั้งแต่เปลี่ยนสาย

dd = -0.0

ถึง:

dd = 0.0

ผลลัพธ์ในการเปรียบเทียบเป็นจริง ...


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