JavaScript'ten C # Sayısal Hassasiyet Kaybı


16

MessagePack ile SignalR kullanarak JavaScript ve C # arasındaki değerleri serileştirirken ve serileştirmeyi kaldırırken, alıcı uçta C # 'da biraz hassas kayıp görüyorum.

Örnek olarak 0.005 değerini JavaScript'ten C # 'a gönderiyorum. Serileştirilmiş değer C # tarafında göründüğünde, 0.004999999888241291tam olarak 0.005 değil, yakın olan değeri alıyorum . JavaScript tarafında değer Numberve ben kullanıyorum C # tarafında double.

JavaScript'in tam olarak kayan nokta sayılarını temsil edemediğini ve sonuçlara neden olabileceğini okudum 0.1 + 0.2 == 0.30000000000000004. Gördüğüm sorunun JavaScript'in bu özelliğiyle ilgili olduğundan şüpheleniyorum.

İşin ilginç tarafı, aynı sorunun diğer tarafa doğru gitmediğini görmem. C # 'den JavaScript'e 0,005 göndermek, JavaScript'te 0,005 değerini verir.

Düzenleme : C # değeri JS hata ayıklayıcı penceresinde kısaltılır. @Pete'in belirttiği gibi, tam olarak 0,5 olmayan bir şeye genişler (0.005000000000000000104083408558). Bu, en azından her iki tarafta tutarsızlık olduğu anlamına gelir.

JSON serileştirme aynı sorunu sahip değil çünkü ben alıcı ortam değerini yerel sayısal türüne ayrıştırma wrt denetimde bırakan dize üzerinden gider.

Her iki tarafta eşleşen değerlere sahip ikili serileştirme kullanmanın bir yolu olup olmadığını merak ediyorum.

Değilse, bu JavaScript ve C # arasında% 100 doğru ikili dönüşüm yapmanın bir yolu olmadığı anlamına mı geliyor?

Kullanılan teknoloji:

  • JavaScript
  • SignalR ve msgpack5 ile Net Çekirdek

Kodum bu gönderiye dayanıyor . Tek fark kullanıyorum ContractlessStandardResolver.Instance.


C # 'daki kayan nokta gösterimi her değer için tam değildir. Serileştirilmiş verilere bir göz atın. Nasıl C # ayrıştırmak?
JeffRSon

C # 'da ne tür kullanıyorsunuz? Double'un böyle bir sorunu olduğu bilinmektedir.
Poul Bak

Signalr ile gelen yerleşik mesaj paketi serilizasyonunu / serileştirmesini ve mesaj paketi entegrasyonunu kullanıyorum.
TGH

Kayan nokta değerleri hiçbir zaman kesin değildir. Kesin değerlere ihtiyacınız varsa, dizeleri (biçimlendirme sorunu) veya tamsayıları kullanın (örneğin, 1000 ile çarparak).
atmin

Serileştirilmiş mesajı kontrol edebilir misiniz? C # bir nesneye dönüşmeden önce js'den aldığınız metin.
Jonny Piazzi

Yanıtlar:


9

GÜNCELLEME

Bu bir sonraki sürümde düzeltildi (5.0.0-önizleme4) .

Orijinal Yanıt

Ben test floatve doubleve ilginç bu özel durumda, sadece double, oysa sorun vardı float(yani 0.005 sunucuda okunur) çalışıyor gibi görünüyor.

İleti baytları üzerinde inceleme, 0.005'in 64 bit kayan nokta Float32Doubleolmasına rağmen 4 bayt / 32 bit IEEE 754 tek hassas kayan nokta sayısı olan tip olarak gönderilmesini önerdi Number.

Yukarıda doğrulanan konsolda aşağıdaki kodu çalıştırın:

msgpack5().encode(Number(0.005))

// Output
Uint8Array(5) [202, 59, 163, 215, 10]

mspack5 , 64 bit kayan noktayı zorlamak için bir seçenek sunar:

msgpack5({forceFloat64:true}).encode(Number(0.005))

// Output
Uint8Array(9) [203, 63, 116, 122, 225, 71, 174, 20, 123]

Ancak, bu forceFloat64seçenek signalr-protocol-msgpack tarafından kullanılmaz .

Bu neden floatsunucu tarafında çalıştığını açıklasa da, şu an için bunun için bir düzeltme yok . Microsoft'un söylediklerini bekleyelim .

Olası çözümler

  • Msgpack5 seçenekleri kesmek? Çatal ve kendi msgpack5 forceFloat64varsayılan doğru ile derlemek ?? Bilmiyorum.
  • Geçin float, sunucu tarafında
  • stringHer iki tarafta da kullanın
  • decimalSunucu tarafına geçin ve özel yazın IFormatterProvider. decimalilkel tür değildir ve IFormatterProvider<decimal>karmaşık tür özellikleri için çağrılır
  • doubleÖzellik değerini almak için yöntem sağlayın ve double-> float-> decimal-> doublehile yapın
  • Aklınıza gelebilecek diğer gerçekçi olmayan çözümler

TL; DR

JS istemcisinin C # arka ucuna tek kayan nokta sayısı gönderme sorunu bilinen bir kayan nokta sorununa neden olur:

// value = 0.00499999988824129, crazy C# :)
var value = (double)0.005f;

doubleYöntemlerde doğrudan kullanım için , sorun bir gelenek tarafından çözülebilir MessagePack.IFormatterResolver:

public class MyDoubleFormatterResolver : IFormatterResolver
{
    public static MyDoubleFormatterResolver Instance = new MyDoubleFormatterResolver();

    private MyDoubleFormatterResolver()
    { }

    public IMessagePackFormatter<T> GetFormatter<T>()
    {
        return MyDoubleFormatter.Instance as IMessagePackFormatter<T>;
    }
}

public sealed class MyDoubleFormatter : IMessagePackFormatter<double>, IMessagePackFormatter
{
    public static readonly MyDoubleFormatter Instance = new MyDoubleFormatter();

    private MyDoubleFormatter()
    {
    }

    public int Serialize(
        ref byte[] bytes,
        int offset,
        double value,
        IFormatterResolver formatterResolver)
    {
        return MessagePackBinary.WriteDouble(ref bytes, offset, value);
    }

    public double Deserialize(
        byte[] bytes,
        int offset,
        IFormatterResolver formatterResolver,
        out int readSize)
    {
        double value;
        if (bytes[offset] == 0xca)
        {
            // 4 bytes single
            // cast to decimal then double will fix precision issue
            value = (double)(decimal)MessagePackBinary.ReadSingle(bytes, offset, out readSize);
            return value;
        }

        value = MessagePackBinary.ReadDouble(bytes, offset, out readSize);
        return value;
    }
}

Ve çözümleyiciyi kullanın:

services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
        {
            MyDoubleFormatterResolver.Instance,
            ContractlessStandardResolver.Instance,
        };
    });

Çözümleyici mükemmel değildir, çünkü decimalo zaman doublesüreci yavaşlatmak için döküm yapmak ve tehlikeli olabilir .

ancak

Açıklamalarda belirttiği OP gereğince, bu olamaz sahip karmaşık türleri kullanarak eğer sorunu çözmek doubledönen özelliklerini.

Daha fazla araştırma, MessagePack-CSharp'ta sorunun nedenini ortaya çıkardı:

// Type: MessagePack.MessagePackBinary
// Assembly: MessagePack, Version=1.9.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be
// MVID: B72E7BA0-FA95-4EB9-9083-858959938BCE
// Assembly location: ...\.nuget\packages\messagepack\1.9.11\lib\netstandard2.0\MessagePack.dll

namespace MessagePack.Decoders
{
  internal sealed class Float32Double : IDoubleDecoder
  {
    internal static readonly IDoubleDecoder Instance = (IDoubleDecoder) new Float32Double();

    private Float32Double()
    {
    }

    public double Read(byte[] bytes, int offset, out int readSize)
    {
      readSize = 5;
      // The problem is here
      // Cast a float value to double like this causes precision loss
      return (double) new Float32Bits(bytes, checked (offset + 1)).Value;
    }
  }
}

Yukarıdaki kod çözücü, tek bir floatsayıyı şuna dönüştürmek gerektiğinde kullanılır double:

// From MessagePackBinary class
MessagePackBinary.doubleDecoders[202] = Float32Double.Instance;

v2

Bu sorun, MessagePack-CSharp'ın v2 sürümlerinde bulunmaktadır. Ben bulundular github üzerinde bir sorunu , sorunun düzeltilmesi için gittiğini olmasa da .


İlginç bulgular. Buradaki bir zorluk, sorunun karmaşık bir nesnede herhangi bir sayıda çift özellik için geçerli olmasıdır, bu yüzden doğrudan çifte hedeflemek zor olacağını düşünüyorum.
TGH

@TGH Evet, haklısın. MessagePack-CSharp'da bir hata olduğuna inanıyorum. Ayrıntılar için güncellenmiş bilgilerime bakın. Şimdilik floatgeçici çözüm olarak kullanmanız gerekebilir . Bunu v2'de düzeltip düzeltmediklerini bilmiyorum. Biraz zamanım olduğunda bakacağım. Ancak, sorun v2 henüz SignalR ile uyumlu değildir. SignalR'nin yalnızca önizleme sürümleri (5.0.0.0- *) v2 kullanabilir.
weichch

Bu da v2'de çalışmıyor. MessagePack-CSharp ile ilgili bir hata oluşturdum.
weichch

@TGH Maalesef github konusundaki tartışmaya göre sunucu tarafında herhangi bir düzeltme yoktur. En iyi düzeltme, istemci tarafının 32 bit yerine 64 bit göndermesini sağlamaktır. Bunu olmaya zorlamak için bir seçenek olduğunu fark ettim, ancak Microsoft bunu (anlayışımdan) ortaya çıkarmıyor. Sadece bir göz atmak istiyorsanız bazı geçici çözümlerle cevap güncellendi. Ve bu konuda iyi şanslar.
weichch

Kulağa ilginç bir ipucu gibi geliyor. Buna bir göz atacağım. Bu konudaki yardımınız için teşekkürler!
TGH

14

Lütfen daha büyük bir hassasiyetle gönderdiğiniz kesin değeri kontrol edin. Diller genellikle daha iyi görünmesi için baskıdaki hassasiyeti sınırlar.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...

Evet, bu konuda haklısın.
TGH
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.