Herkes bu garip davranışı C # imzalı yüzen ile açıklayabilir?


247

İşte yorum içeren örnek:

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

Peki, bunun hakkında ne düşünüyorsun?


2
Şeyleri yabancı yapmak için c.d.Equals(d.d)için değerlendirir trueyaptığı gibic.f.Equals(d.f)
Justin Nießner

2
Şamandıralar gibi kesin karşılaştırma ile karşılaştırmayın. Bu sadece kötü bir fikir.
Thorsten79

6
@ Thorsten79: Bu burada nasıl ilgili?
Ben M

2
Bu çok garip. F yerine uzun bir double kullanmak aynı davranışı getirir. Ve başka bir kısa alan eklemek tekrar düzeltir ...
Jens

1
Tuhaf - sadece her ikisi de aynı tipte (şamandıra veya çift) olduğunda görülür. Birini kayan (veya ondalık) olarak değiştirin ve D2, D1 ile aynı şekilde çalışır.
tvanfosson

Yanıtlar:


387

Hata aşağıdaki iki satırda System.ValueType: (Referans kaynağına adım attım)

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

(Her iki yöntem de [MethodImpl(MethodImplOptions.InternalCall)])

Tüm alanlar 8 bayt genişliğinde olduğunda, CanCompareBitsyanlışlıkla true değerini döndürerek iki farklı, ancak anlamsal olarak özdeş değerin bitsel olarak karşılaştırılmasına yol açar.

En az bir alan 8 bayt genişliğinde değilse, CanCompareBitsfalse değerini döndürür ve kod, alanlar üzerinde döngü oluşturmak ve Equalsher bir değeri çağırmak için yansıma kullanmaya devam eder ; bu, doğru -0.0olarak eşit kabul edilir 0.0.

CanCompareBitsSSCLI kaynak :

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 adım mı? Bu çok sert kardeşim.
Pierreten

2
"8 bayt genişlik" in önemini açıklamıyorsunuz. 4 baytlık alanların tümüne sahip bir yapı aynı sonuca sahip olmaz mı? Tek bir 4 baytlık alana ve 8 baytlık bir alana sahip olmanın sadece tetiklediğini tahmin ediyorum IsNotTightlyPacked.
Gabe

1
@Gabe bunu daha önce yazdımThe 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 artık açık kaynaklı yazılım olduğundan, ValueTypeHelper :: CanCompareBits'in Temel CLR uygulamasına bir bağlantı . Uygulama, gönderdiğiniz referans kaynağından biraz değiştirildiği için yanıtınızı güncellemek istemiyordu.
17'de

59

Cevabı http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx adresinde buldum .

Çekirdek parçası kaynak yorumdur CanCompareBits, ValueType.Equalskullanımı olup olmadığını belirlemek için kullanır memcmptarzı karşılaştırma:

CanCompareBits'in yorumu, "valueetype işaretçi içermiyorsa ve sıkıca paketlenmişse true döndürür" yazıyor. Ve FastEqualsCheck karşılaştırmayı hızlandırmak için "memcmp" kullanın.

Yazar, OP tarafından açıklanan sorunu tam olarak belirtmektedir:

Sadece şamandıra içeren bir yapıya sahip olduğunuzu düşünün. Biri +0.0, diğeri -0.0 içeriyorsa ne olur? Aynı olmalılar, ancak temeldeki ikili gösterim farklıdır. Eşittir yöntemini geçersiz kılan başka bir yapıyı iç içe yerleştirirseniz, bu optimizasyon da başarısız olur.


Acaba davranışı eğer Equals(Object)için double, floatve Decimal.net erken taslaklar sırasında değişti; Ben sanal X.Equals((Object)Y)sadece sadece truene zaman Xve Yayırt edilemez geri dönmek için, bu yöntem diğer aşırı yüklerin davranışı (özellikle örtük tip zorlama nedeniyle, aşırı yüklenmiş Equalsyöntemler bile bir denklik ilişkisi tanımlamak değil ki) maç daha önemli olduğunu düşünürdüm !, örneğin 1.0f.Equals(1.0)yanlış verir, ancak 1.0.Equals(1.0f)doğru verir!) Gerçek sorun IMHO, yapıların karşılaştırılmasıyla ilgili değildir ...
Supercat

1
... ama bu değer türlerinin denklikten Equalsbaşka bir şey ifade etmek için geçersiz kılınmasıyla . Örneğin, değişmez bir nesneyi alan ve henüz önbelleğe alınmamışsa ToString, bunu gerçekleştiren ve sonucu önbelleğe alan bir yöntem yazmak istediğini varsayalım . önbelleğe alınmışsa, önbelleğe alınmış dizeyi döndürmeniz yeterlidir. Yapılması mantıksız bir şey değil, ancak Decimaliki değer eşit olabilir, ancak farklı dizeler verebilir , çünkü kötü başarısız olur .
supercat

52

Vilx'in varsayımı doğrudur. "CanCompareBits" in yaptığı, söz konusu değer türünün bellekte "sıkıca paketlenmiş" olup olmadığını kontrol etmektir. Sıkı paketlenmiş bir yapı, sadece yapıyı oluşturan ikili bitleri karşılaştırarak karşılaştırılır; gevşek bir şekilde paketlenmiş bir yapı, tüm üyelere Eşittir çağrılarak karşılaştırılır.

Bu, SLaks'ın iki katına çıkan yapılarla karşı karşıya kaldığı gözlemini açıklar; bu tür yapılar her zaman sıkıca paketlenir.

Ne yazık ki burada gördüğümüz gibi, bu semantik bir fark getiriyor çünkü çiftlerin bitsel karşılaştırması ve çiftlerin eşit karşılaştırması farklı sonuçlar veriyor.


3
O zaman neden bir hata değil? MS her zaman değer türlerinde Eşittirleri geçersiz kılmayı önerse de.
Alexander Efimov

14
Lanet olsun beni alt etti. CLR'nin içleri konusunda uzman değilim.
Eric Lippert

4
... Değil mi? Elbette C # içsel bilginiz, CLR'nin nasıl çalıştığı hakkında önemli bilgilere yol açacaktır.
KaptanCasey

37
@CaptainCasey: C # derleyicisinin iç kısımlarını inceleyerek beş yıl geçirdim ve muhtemelen toplamda birkaç saat CLR'nin iç kısımlarını inceledim. Unutmayın, ben CLR'nin tüketicisiyim ; Kamusal yüzey alanını oldukça iyi anlıyorum, ama iç kısımları benim için bir kara kutu.
Eric Lippert

1
Benim hatam, CLR ve VB / C # derleyicilerinin daha sıkı bir şekilde eşleştiğini düşündüm ... bu yüzden C # / VB -> CIL -> CLR
CaptainCasey

22

Yarım cevap:

Reflektör bize bunun ValueType.Equals()böyle bir şey yaptığını söyler :

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

Maalesef hem CanCompareBits()ve FastEquals()(her ikisi de statik yöntemleri) (extern olan [MethodImpl(MethodImplOptions.InternalCall)]) ve hiçbir kaynak var.

Bir vakanın neden bitlerle karşılaştırılabileceğini ve diğerinin yapamayacağını tahmin etmeye geri dönün (hizalama sorunları belki?)


17

Bu mu Mono GOK değerleri 2.4.2.3 ile, benim için doğrudur ver.


5
Evet, Mono'da da denedim ve bu da bana gerçek oluyor. MS'in içinde sihir yaptığı gibi görünüyor :)
Alexander Efimov

3
ilginç, hepimiz Mono'ya gönderiyoruz?
WeNeedAnswers

14

Daha basit test durumu:

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

DÜZENLEME : Hata aynı zamanda kayan noktalarda da olur, ancak yalnızca yapıdaki alanlar 8 baytın katlarına kadar eklenirse olur.


Giden bir iyileştirici kuralı gibi görünüyor: eğer biraz-karşılaştırmak, başka yapmak ayrı double.Equal çağrıları yapmak daha onun bütün çiftler
Henk Holterman

Bunun burada sunulan sorunun olduğu gibi aynı test örneği olduğunu düşünmüyorum, Bad.f için varsayılan değerin 0 değil, diğer durumda Int vs. Double sorunu olduğu düşünülüyor.
Driss Zouak

6
@Driss: varsayılan değeri double olan 0 . Yanılıyorsun.
SLaks

10

Sadece sinyal bitinden 0.0farklı olması gerektiğinden , biraz bit karşılaştırması ile ilgili olmalıdır -0.0.


5

…bunun hakkında ne düşünüyorsun?

Değer türlerinde her zaman Equals ve GetHashCode'u geçersiz kılın. Hızlı ve doğru olacak.


Bunun sadece eşitlik söz konusu olduğunda gerekli olduğu konusunda bir uyarıdan başka, tam da böyle düşünüyordum. En yüksek oylanan cevaplar gibi varsayılan değer türü eşitlik davranışının tuhaflıklarına bakmak kadar eğlenceli, CA1815'in var olmasının bir nedeni var.
Joe Amenta

@ JoeAmenta Geç cevap verdiğim için üzgünüm. Bana göre (sadece benim görüşüme göre, elbette), eşitlik her zaman değer türleriyle ilgilidir. Varsayılan eşitlik uygulaması yaygın durumlarda kabul edilemez. ( ) Çok özel durumlar hariç. Çok. Çok özel. Ne yaptığını ve nedenini tam olarak bildiğinde.
Viacheslav Ivanov

Değer türleri için eşitlik kontrollerini geçersiz kılmanın çok az istisna dışında neredeyse her zaman mümkün ve anlamlı olduğunu ve genellikle bunu kesinlikle daha doğru hale getireceğini kabul ediyoruz. "Alakalı" kelimesiyle aktarmaya çalıştığım nokta, örnekleri hiçbir zaman eşitlik için diğer örneklerle karşılaştırılmayacak bazı değer türlerinin bulunmasıydı, bu nedenle geçersiz kılma, sürdürülmesi gereken ölü kodla sonuçlanacaktır. Bunlar (ve bahsettiğiniz tuhaf özel durumlar) atlayabileceğim tek yerler olurdu.
Joe Amenta

4

Bu 10 yıllık hata için sadece bir güncelleme: .NET Core 2.1.0'da yayınlanacak olan .NET Core'da düzeltildi ( Feragatname : Bu PR'nin yazarıyım).

Blog yazısı hata açıkladı ve nasıl düzelttim.


2

D2'yi böyle yaparsanız

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

bu doğru.

eğer böyle yaparsan

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

Hala yanlış.

i yapı sadece çiftler tutan eğer yanlış gibi t görünüyor.


1

Satırı değiştirdiğinden sıfır ile ilgili olmalıdır

gg = -0,0

için:

gg = 0.0

karşılaştırmanın doğru olduğu ...


Tersine, NaN'ler aslında aynı bit desenini kullandıklarında, bir değişiklik için birbirleriyle eşit olabilirler.
harold
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.