Bất cứ ai cũng có thể giải thích hành vi kỳ lạ này với số float đã ký trong C #?


247

Dưới đây là ví dụ với ý kiến:

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        
    }
}

Vậy, bạn nghĩ gì về điều này?


2
Để làm cho những điều lạ lùng c.d.Equals(d.d)đánh giá truenhư vậyc.f.Equals(d.f)
Justin Niessner

2
Đừng so sánh số float với so sánh chính xác như .Equals. Nó đơn giản là một ý tưởng tồi.
Thorsten79

6
@ Thorsten79: Làm thế nào là có liên quan ở đây?
Ben M

2
Điều này là lạ nhất. Sử dụng một thay vì dài gấp đôi cho f giới thiệu hành vi tương tự. Và thêm một trường ngắn khác sửa nó một lần nữa ...
Jens

1
Lạ - dường như chỉ xảy ra khi cả hai cùng loại (float hoặc double). Thay đổi một thành float (hoặc thập phân) và D2 ​​hoạt động giống như D1.
tvanfosson

Câu trả lời:


387

Lỗi nằm ở hai dòng System.ValueTypesau: (Tôi bước vào nguồn tham khảo)

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

(Cả hai phương pháp là [MethodImpl(MethodImplOptions.InternalCall)])

Khi tất cả các trường có chiều rộng 8 byte, CanCompareBitstrả về nhầm là true, dẫn đến so sánh bitwise của hai giá trị khác nhau, nhưng giống hệt nhau về mặt ngữ nghĩa.

Khi có ít nhất một trường không rộng 8 byte, CanCompareBitstrả về false và mã tiến hành sử dụng phản xạ để lặp qua các trường và gọi Equalscho mỗi giá trị, xử lý chính xác -0.0bằng 0.0.

Đây là nguồn cho CanCompareBitstừ 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

158
Bước vào System.ValueType? Đó là người bạn khá khó tính.
Pierreten

2
Bạn không giải thích tầm quan trọng của "8 byte rộng" là gì. Một cấu trúc với tất cả các trường 4 byte sẽ không có cùng kết quả? Tôi đoán rằng có một trường 4 byte đơn và một trường 8 byte chỉ kích hoạt IsNotTightlyPacked.
Gabe

1
@Gabe Tôi đã viết trước đó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
Hiện tại, .NET là phần mềm nguồn mở, đây là một liên kết đến việc triển khai Core CLR của ValueTypeHelper :: CanCompareBits . Không muốn cập nhật câu trả lời của bạn vì việc triển khai hơi thay đổi so với nguồn tham khảo bạn đã đăng.
IInspectable

59

Tôi đã tìm thấy câu trả lời tại http://bloss.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

Phần cốt lõi là nhận xét nguồn CanCompareBits, ValueType.Equalssử dụng để xác định xem có nên sử dụng memcmpso sánh kiểu hay không:

Nhận xét của CanCompareBits cho biết "Trả về đúng nếu giá trị không chứa con trỏ và được đóng gói chặt chẽ". Và FastEqualsCheck sử dụng "memcmp" để tăng tốc độ so sánh.

Tác giả tiếp tục nêu chính xác vấn đề được mô tả bởi OP:

Hãy tưởng tượng bạn có một cấu trúc chỉ chứa một cái phao. Điều gì sẽ xảy ra nếu một cái chứa +0.0 và cái kia chứa -0.0? Chúng nên giống nhau, nhưng biểu diễn nhị phân cơ bản là khác nhau. Nếu bạn lồng cấu trúc khác ghi đè phương thức Equals, tối ưu hóa đó cũng sẽ thất bại.


Tôi tự hỏi nếu hành vi của Equals(Object)cho double, floatDecimalthay đổi trong bản dự thảo đầu tiên của .net; Tôi nghĩ rằng điều quan trọng là X.Equals((Object)Y)chỉ có ảo trở lại truekhi XYkhông thể phân biệt được, hơn là phương thức đó phù hợp với hành vi của các tình trạng quá tải khác (đặc biệt là vì ép buộc kiểu ngầm, Equalscác phương thức quá tải thậm chí không xác định mối quan hệ tương đương !, ví dụ: 1.0f.Equals(1.0)mang lại sai, nhưng 1.0.Equals(1.0f)mang lại kết quả đúng!) Vấn đề thực sự IMHO không nằm ở cách so sánh các cấu trúc ...
supercat

1
... nhưng với cách mà các loại giá trị đó ghi đè lên Equalscó nghĩa là một cái gì đó khác hơn là tương đương. Ví dụ, giả sử, người ta muốn viết một phương thức lấy một đối tượng bất biến và, nếu nó chưa được lưu vào bộ đệm, thực hiện ToStringtrên nó và lưu trữ kết quả; nếu nó đã được lưu trữ, chỉ cần trả về chuỗi đã lưu. Không phải là một điều không hợp lý để làm, nhưng nó sẽ thất bại nặng nề Decimalvì hai giá trị có thể so sánh bằng nhau nhưng mang lại các chuỗi khác nhau.
supercat

52

Phỏng đoán của Vilx là đúng. "CanCompareBits" làm gì là kiểm tra xem liệu loại giá trị được đề cập có "được đóng gói chặt chẽ" trong bộ nhớ hay không. Một cấu trúc đóng gói chặt chẽ được so sánh bằng cách đơn giản so sánh các bit nhị phân tạo nên cấu trúc; một cấu trúc đóng gói lỏng lẻo được so sánh bằng cách gọi Equals trên tất cả các thành viên.

Điều này giải thích cho quan sát của SLaks rằng nó lặp lại với các cấu trúc đều tăng gấp đôi; cấu trúc như vậy luôn luôn được đóng gói chặt chẽ.

Thật không may như chúng ta đã thấy ở đây, điều đó giới thiệu một sự khác biệt về ngữ nghĩa bởi vì so sánh bitwise của phép nhân đôi và so sánh bằng nhau cho kết quả khác nhau.


3
Vậy thì tại sao nó không phải là một lỗi? Mặc dù MS khuyên bạn nên ghi đè Bằng trên các loại giá trị luôn.
Alexander Efimov

14
Nhịp đập ra khỏi tôi. Tôi không phải là một chuyên gia về nội bộ của CLR.
Eric Lippert

4
... Bạn không? Chắc chắn kiến ​​thức của bạn về nội bộ C # sẽ dẫn đến kiến ​​thức đáng kể về cách CLR hoạt động.
Thuyền trưởngCasey

37
@CaptainCasey: Tôi đã dành năm năm để nghiên cứu các phần bên trong của trình biên dịch C # và có lẽ trong tổng số một vài giờ để nghiên cứu các phần bên trong của CLR. Hãy nhớ rằng, tôi là người tiêu dùng của CLR; Tôi hiểu diện tích bề mặt công cộng của nó khá tốt, nhưng bên trong nó là một hộp đen đối với tôi.
Eric Lippert

1
Lỗi của tôi, tôi nghĩ rằng trình biên dịch CLR và VB / C # được kết hợp chặt chẽ hơn ... vì vậy C # / VB -> CIL -> CLR
CaptainCasey

22

Một nửa câu trả lời:

Reflector cho chúng ta biết rằng ValueType.Equals()làm một cái gì đó như thế này:

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

Thật không may, cả hai CanCompareBits()FastEquals()(cả hai phương thức tĩnh) đều là extern ( [MethodImpl(MethodImplOptions.InternalCall)]) và không có nguồn nào khả dụng.

Quay lại để đoán tại sao một trường hợp có thể được so sánh bằng bit và trường hợp khác không thể (vấn đề căn chỉnh có thể?)


17

Điều đó đúng với tôi, với gmcs 2.4.2.3 của Mono.


5
Vâng, tôi cũng đã thử nó trong Mono và nó cũng cho tôi sự thật. Có vẻ như MS thực hiện một số phép thuật bên trong :)
Alexander Efimov

3
Thật thú vị, tất cả chúng ta gửi đến Mono?
WeNeedAnswers

14

Trường hợp thử nghiệm đơn giản hơn:

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;
}

EDIT : Lỗi cũng xảy ra với float, nhưng chỉ xảy ra nếu các trường trong struct thêm tối đa 8 byte.


Trông giống như một quy tắc tối ưu hóa: nếu tất cả đều tăng gấp đôi so với thực hiện so sánh bit, thì các cuộc gọi khác sẽ thực hiện gấp đôi. Các cuộc gọi khác nhau
Henk Holterman

Tôi không nghĩ đây là trường hợp thử nghiệm giống như vấn đề được trình bày ở đây dường như là giá trị mặc định cho Bad.f không phải là 0, trong khi trường hợp khác dường như là vấn đề Int so với Double.
Driss Zouak

6
@Driss: Giá trị mặc định cho double 0 . Bạn sai rồi.
SLaks

10

Nó phải liên quan đến so sánh từng bit một, vì 0.0chỉ khác nhau -0.0bởi bit tín hiệu.


5

…bạn nghĩ gì về điều này?

Luôn ghi đè bằng và GetHashCode trên các loại giá trị. Nó sẽ nhanh và chính xác.


Khác với một lời cảnh báo rằng điều này chỉ cần thiết khi sự bình đẳng có liên quan, đây chính xác là những gì tôi đã nghĩ. Thật thú vị khi xem xét các yêu cầu của hành vi bình đẳng loại giá trị mặc định như các câu trả lời được bình chọn cao nhất, có một lý do tại sao CA1815 tồn tại.
Joe Amenta

@JoeAmenta Xin lỗi vì trả lời muộn. Theo quan điểm của tôi (tất nhiên chỉ theo quan điểm của tôi), đẳng thức luôn luôn ( ) phù hợp với các loại giá trị. Thực hiện bình đẳng mặc định không được chấp nhận trong các trường hợp phổ biến. ( ) Trừ những trường hợp rất đặc biệt. Rất. Rất đặc biệt. Khi bạn biết chính xác những gì bạn đang làm và tại sao.
Viacheslav Ivanov

Tôi nghĩ rằng chúng tôi đồng ý rằng việc ghi đè các kiểm tra bằng cho các loại giá trị hầu như luôn luôn có thể và có ý nghĩa với rất ít ngoại lệ, và thường sẽ làm cho nó đúng hơn. Điểm tôi đang cố gắng truyền đạt bằng từ "có liên quan" là có một số loại giá trị mà các thể hiện của chúng sẽ không bao giờ được so sánh với các thể hiện khác cho sự bình đẳng, do đó, việc ghi đè sẽ dẫn đến mã chết cần được duy trì. Những người (và những trường hợp đặc biệt kỳ lạ mà bạn ám chỉ) sẽ là nơi duy nhất tôi bỏ qua.
Joe Amenta

4

Chỉ là một bản cập nhật cho lỗi 10 năm tuổi này: nó đã được sửa ( Tuyên bố miễn trừ trách nhiệm : Tôi là tác giả của PR này) trong .NET Core có thể sẽ được phát hành trong .NET Core 2.1.0.

Bài đăng trên blog đã giải thích lỗi và cách tôi sửa nó.


2

Nếu bạn làm D2 như thế này

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

đúng rồi.

nếu bạn làm nó như thế này

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

Nó vẫn sai.

Tôi dường như sai nếu cấu trúc chỉ giữ gấp đôi.


1

Nó phải là không liên quan, kể từ khi thay đổi dòng

đ = -0,0

đến:

đ = 0,0

kết quả so sánh là đúng ...


Ngược lại, NaN có thể so sánh bằng nhau để thay đổi, khi họ thực sự sử dụng cùng một mẫu bit.
harold
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.