From f3e9a65d758d47633a161e526b35dea22bac9a5c Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Wed, 25 Oct 2017 16:03:23 +0100 Subject: Compare floating point values bitwise in C# This is the manual code part of the Google.Protobuf library, and tests. Some tests will fail until codegen is changed and rerun. --- Makefile.am | 3 + .../Collections/MapFieldTest.cs | 43 +++++++ .../Collections/ProtobufEqualityComparersTest.cs | 124 ++++++++++++++++++++ .../Collections/RepeatedFieldTest.cs | 13 +++ .../Google.Protobuf.Test/GeneratedMessageTest.cs | 11 ++ csharp/src/Google.Protobuf.Test/SampleNaNs.cs | 53 +++++++++ .../WellKnownTypes/WrappersTest.cs | 11 ++ csharp/src/Google.Protobuf/Collections/MapField.cs | 22 ++-- .../Collections/ProtobufEqualityComparers.cs | 130 +++++++++++++++++++++ .../Google.Protobuf/Collections/RepeatedField.cs | 5 +- csharp/src/Google.Protobuf/FieldCodec.cs | 4 +- 11 files changed, 405 insertions(+), 14 deletions(-) create mode 100644 csharp/src/Google.Protobuf.Test/Collections/ProtobufEqualityComparersTest.cs create mode 100644 csharp/src/Google.Protobuf.Test/SampleNaNs.cs create mode 100644 csharp/src/Google.Protobuf/Collections/ProtobufEqualityComparers.cs diff --git a/Makefile.am b/Makefile.am index 5ad2aa8b..0729d51b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -84,6 +84,7 @@ csharp_EXTRA_DIST= \ csharp/src/Google.Protobuf.Test/CodedInputStreamTest.cs \ csharp/src/Google.Protobuf.Test/CodedOutputStreamTest.cs \ csharp/src/Google.Protobuf.Test/Collections/MapFieldTest.cs \ + csharp/src/Google.Protobuf.Test/Collections/ProtobufEqualityComparersTest.cs \ csharp/src/Google.Protobuf.Test/Collections/RepeatedFieldTest.cs \ csharp/src/Google.Protobuf.Test/Compatibility/PropertyInfoExtensionsTest.cs \ csharp/src/Google.Protobuf.Test/Compatibility/StreamExtensionsTest.cs \ @@ -104,6 +105,7 @@ csharp_EXTRA_DIST= \ csharp/src/Google.Protobuf.Test/Reflection/TypeRegistryTest.cs \ csharp/src/Google.Protobuf.Test/SampleEnum.cs \ csharp/src/Google.Protobuf.Test/SampleMessages.cs \ + csharp/src/Google.Protobuf.Test/SampleNaNs.cs \ csharp/src/Google.Protobuf.Test/TestCornerCases.cs \ csharp/src/Google.Protobuf.Test/TestProtos/ForeignMessagePartial.cs \ csharp/src/Google.Protobuf.Test/TestProtos/MapUnittestProto3.cs \ @@ -126,6 +128,7 @@ csharp_EXTRA_DIST= \ csharp/src/Google.Protobuf/CodedOutputStream.ComputeSize.cs \ csharp/src/Google.Protobuf/CodedOutputStream.cs \ csharp/src/Google.Protobuf/Collections/MapField.cs \ + csharp/src/Google.Protobuf/Collections/ProtobufEqualityComparers.cs \ csharp/src/Google.Protobuf/Collections/ReadOnlyDictionary.cs \ csharp/src/Google.Protobuf/Collections/RepeatedField.cs \ csharp/src/Google.Protobuf/Compatibility/PropertyInfoExtensions.cs \ diff --git a/csharp/src/Google.Protobuf.Test/Collections/MapFieldTest.cs b/csharp/src/Google.Protobuf.Test/Collections/MapFieldTest.cs index 68b4de45..8791dffc 100644 --- a/csharp/src/Google.Protobuf.Test/Collections/MapFieldTest.cs +++ b/csharp/src/Google.Protobuf.Test/Collections/MapFieldTest.cs @@ -540,6 +540,49 @@ namespace Google.Protobuf.Collections Assert.Throws(() => map.ToString()); } + [Test] + public void NaNValuesComparedBitwise() + { + var map1 = new MapField + { + { "x", SampleNaNs.Regular }, + { "y", SampleNaNs.SignallingFlipped } + }; + + var map2 = new MapField + { + { "x", SampleNaNs.Regular }, + { "y", SampleNaNs.PayloadFlipped } + }; + + var map3 = new MapField + { + { "x", SampleNaNs.Regular }, + { "y", SampleNaNs.SignallingFlipped } + }; + + EqualityTester.AssertInequality(map1, map2); + EqualityTester.AssertEquality(map1, map3); + Assert.True(map1.Values.Contains(SampleNaNs.SignallingFlipped)); + Assert.False(map2.Values.Contains(SampleNaNs.SignallingFlipped)); + } + + // This wouldn't usually happen, as protos can't use doubles as map keys, + // but let's be consistent. + [Test] + public void NaNKeysComparedBitwise() + { + var map = new MapField + { + { SampleNaNs.Regular, "x" }, + { SampleNaNs.SignallingFlipped, "y" } + }; + Assert.AreEqual("x", map[SampleNaNs.Regular]); + Assert.AreEqual("y", map[SampleNaNs.SignallingFlipped]); + string ignored; + Assert.False(map.TryGetValue(SampleNaNs.PayloadFlipped, out ignored)); + } + #if !NET35 [Test] public void IDictionaryKeys_Equals_IReadOnlyDictionaryKeys() diff --git a/csharp/src/Google.Protobuf.Test/Collections/ProtobufEqualityComparersTest.cs b/csharp/src/Google.Protobuf.Test/Collections/ProtobufEqualityComparersTest.cs new file mode 100644 index 00000000..c76d7ca1 --- /dev/null +++ b/csharp/src/Google.Protobuf.Test/Collections/ProtobufEqualityComparersTest.cs @@ -0,0 +1,124 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2017 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; +using static Google.Protobuf.Collections.ProtobufEqualityComparers; + +namespace Google.Protobuf.Collections +{ + public class ProtobufEqualityComparersTest + { + private static readonly double[] doubles = + { + 0, + 1, + 1.5, + -1.5, + double.PositiveInfinity, + double.NegativeInfinity, + // Three different types of NaN... + SampleNaNs.Regular, + SampleNaNs.SignallingFlipped, + SampleNaNs.PayloadFlipped + }; + + [Test] + public void GetEqualityComparer_Default() + { + // It's more pain than it's worth to try to parameterize these tests. + Assert.AreSame(EqualityComparer.Default, GetEqualityComparer()); + Assert.AreSame(EqualityComparer.Default, GetEqualityComparer()); + Assert.AreSame(EqualityComparer.Default, GetEqualityComparer()); + Assert.AreSame(EqualityComparer.Default, GetEqualityComparer()); + } + + [Test] + public void GetEqualityComparer_NotDefault() + { + // It's more pain than it's worth to try to parameterize these tests. + Assert.AreSame(BitwiseDoubleEqualityComparer, GetEqualityComparer()); + Assert.AreSame(BitwiseSingleEqualityComparer, GetEqualityComparer()); + Assert.AreSame(BitwiseNullableDoubleEqualityComparer, GetEqualityComparer()); + Assert.AreSame(BitwiseNullableSingleEqualityComparer, GetEqualityComparer()); + } + + [Test] + public void DoubleComparisons() + { + ValidateEqualityComparer(BitwiseDoubleEqualityComparer, doubles); + } + + [Test] + public void NullableDoubleComparisons() + { + ValidateEqualityComparer(BitwiseNullableDoubleEqualityComparer, doubles.Select(d => (double?) d).Concat(new double?[] { null })); + } + + [Test] + public void SingleComparisons() + { + ValidateEqualityComparer(BitwiseSingleEqualityComparer, doubles.Select(d => (float) d)); + } + + [Test] + public void NullableSingleComparisons() + { + ValidateEqualityComparer(BitwiseNullableSingleEqualityComparer, doubles.Select(d => (float?) d).Concat(new float?[] { null })); + } + + private static void ValidateEqualityComparer(EqualityComparer comparer, IEnumerable values) + { + var array = values.ToArray(); + // Each value should be equal to itself, but not to any other value. + for (int i = 0; i < array.Length; i++) + { + for (int j = 0; j < array.Length; j++) + { + if (i == j) + { + Assert.IsTrue(comparer.Equals(array[i], array[j]), + "{0} should be equal to itself", array[i], array[j]); + } + else + { + Assert.IsFalse(comparer.Equals(array[i], array[j]), + "{0} and {1} should not be equal", array[i], array[j]); + Assert.AreNotEqual(comparer.GetHashCode(array[i]), comparer.GetHashCode(array[j]), + "Hash codes for {0} and {1} should not be equal", array[i], array[j]); + } + } + } + } + } +} diff --git a/csharp/src/Google.Protobuf.Test/Collections/RepeatedFieldTest.cs b/csharp/src/Google.Protobuf.Test/Collections/RepeatedFieldTest.cs index 6852f75f..129923b6 100644 --- a/csharp/src/Google.Protobuf.Test/Collections/RepeatedFieldTest.cs +++ b/csharp/src/Google.Protobuf.Test/Collections/RepeatedFieldTest.cs @@ -742,5 +742,18 @@ namespace Google.Protobuf.Collections var text = list.ToString(); Assert.AreEqual(text, "[ { \"foo\": 20 } ]", message.ToString()); } + + [Test] + public void NaNValuesComparedBitwise() + { + var list1 = new RepeatedField { SampleNaNs.Regular, SampleNaNs.SignallingFlipped }; + var list2 = new RepeatedField { SampleNaNs.Regular, SampleNaNs.PayloadFlipped }; + var list3 = new RepeatedField { SampleNaNs.Regular, SampleNaNs.SignallingFlipped }; + + EqualityTester.AssertInequality(list1, list2); + EqualityTester.AssertEquality(list1, list3); + Assert.True(list1.Contains(SampleNaNs.SignallingFlipped)); + Assert.False(list2.Contains(SampleNaNs.SignallingFlipped)); + } } } diff --git a/csharp/src/Google.Protobuf.Test/GeneratedMessageTest.cs b/csharp/src/Google.Protobuf.Test/GeneratedMessageTest.cs index 8b153d69..8292d3e7 100644 --- a/csharp/src/Google.Protobuf.Test/GeneratedMessageTest.cs +++ b/csharp/src/Google.Protobuf.Test/GeneratedMessageTest.cs @@ -719,5 +719,16 @@ namespace Google.Protobuf JsonFormatter.Default.Format(message, writer); Assert.AreEqual("{ \"c\": 31 }", writer.ToString()); } + + [Test] + public void NaNComparisons() + { + var message1 = new TestAllTypes { SingleDouble = SampleNaNs.Regular }; + var message2 = new TestAllTypes { SingleDouble = SampleNaNs.PayloadFlipped }; + var message3 = new TestAllTypes { SingleDouble = SampleNaNs.Regular }; + + EqualityTester.AssertInequality(message1, message2); + EqualityTester.AssertEquality(message1, message3); + } } } \ No newline at end of file diff --git a/csharp/src/Google.Protobuf.Test/SampleNaNs.cs b/csharp/src/Google.Protobuf.Test/SampleNaNs.cs new file mode 100644 index 00000000..08b50191 --- /dev/null +++ b/csharp/src/Google.Protobuf.Test/SampleNaNs.cs @@ -0,0 +1,53 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2017 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; + +namespace Google.Protobuf +{ + /// + /// Samples of different not-a-number values, for testing equality comparisons. + /// + public static class SampleNaNs + { + public static double Regular { get; } = double.NaN; + + // Signalling bit is inverted compared with double.NaN. Doesn't really matter + // whether that makes it quiet or signalling - it's different. + public static double SignallingFlipped { get; } = + BitConverter.Int64BitsToDouble(BitConverter.DoubleToInt64Bits(double.NaN) ^ -0x8000_0000_0000_0000L); + + // A bit in the middle of the mantissa is flipped; this difference is preserved when casting to float. + public static double PayloadFlipped { get; } = + BitConverter.Int64BitsToDouble(BitConverter.DoubleToInt64Bits(double.NaN) ^ 0x1_0000_0000L); + } +} diff --git a/csharp/src/Google.Protobuf.Test/WellKnownTypes/WrappersTest.cs b/csharp/src/Google.Protobuf.Test/WellKnownTypes/WrappersTest.cs index 5b7185dc..8ed55744 100644 --- a/csharp/src/Google.Protobuf.Test/WellKnownTypes/WrappersTest.cs +++ b/csharp/src/Google.Protobuf.Test/WellKnownTypes/WrappersTest.cs @@ -417,5 +417,16 @@ namespace Google.Protobuf.WellKnownTypes TestWellKnownTypes.Descriptor.Fields[TestWellKnownTypes.StringFieldFieldNumber].Accessor.Clear(message); Assert.IsNull(message.StringField); } + + [Test] + public void NaNComparisons() + { + var message1 = new TestWellKnownTypes { DoubleField = SampleNaNs.Regular }; + var message2 = new TestWellKnownTypes { DoubleField = SampleNaNs.PayloadFlipped }; + var message3 = new TestWellKnownTypes { DoubleField = SampleNaNs.Regular }; + + EqualityTester.AssertInequality(message1, message2); + EqualityTester.AssertEquality(message1, message3); + } } } diff --git a/csharp/src/Google.Protobuf/Collections/MapField.cs b/csharp/src/Google.Protobuf/Collections/MapField.cs index 8dac8e30..dbbcc148 100644 --- a/csharp/src/Google.Protobuf/Collections/MapField.cs +++ b/csharp/src/Google.Protobuf/Collections/MapField.cs @@ -71,9 +71,12 @@ namespace Google.Protobuf.Collections , IReadOnlyDictionary #endif { + private static readonly EqualityComparer ValueEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer(); + private static readonly EqualityComparer KeyEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer(); + // TODO: Don't create the map/list until we have an entry. (Assume many maps will be empty.) private readonly Dictionary>> map = - new Dictionary>>(); + new Dictionary>>(KeyEqualityComparer); private readonly LinkedList> list = new LinkedList>(); /// @@ -131,11 +134,8 @@ namespace Google.Protobuf.Collections return map.ContainsKey(key); } - private bool ContainsValue(TValue value) - { - var comparer = EqualityComparer.Default; - return list.Any(pair => comparer.Equals(pair.Value, value)); - } + private bool ContainsValue(TValue value) => + list.Any(pair => ValueEqualityComparer.Equals(pair.Value, value)); /// /// Removes the entry identified by the given key from the map. @@ -293,8 +293,7 @@ namespace Google.Protobuf.Collections bool ICollection>.Contains(KeyValuePair item) { TValue value; - return TryGetValue(item.Key, out value) - && EqualityComparer.Default.Equals(item.Value, value); + return TryGetValue(item.Key, out value) && ValueEqualityComparer.Equals(item.Value, value); } /// @@ -363,11 +362,12 @@ namespace Google.Protobuf.Collections /// public override int GetHashCode() { - var valueComparer = EqualityComparer.Default; + var keyComparer = KeyEqualityComparer; + var valueComparer = ValueEqualityComparer; int hash = 0; foreach (var pair in list) { - hash ^= pair.Key.GetHashCode() * 31 + valueComparer.GetHashCode(pair.Value); + hash ^= keyComparer.GetHashCode(pair.Key) * 31 + valueComparer.GetHashCode(pair.Value); } return hash; } @@ -394,7 +394,7 @@ namespace Google.Protobuf.Collections { return false; } - var valueComparer = EqualityComparer.Default; + var valueComparer = ValueEqualityComparer; foreach (var pair in this) { TValue value; diff --git a/csharp/src/Google.Protobuf/Collections/ProtobufEqualityComparers.cs b/csharp/src/Google.Protobuf/Collections/ProtobufEqualityComparers.cs new file mode 100644 index 00000000..13ef60fc --- /dev/null +++ b/csharp/src/Google.Protobuf/Collections/ProtobufEqualityComparers.cs @@ -0,0 +1,130 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2017 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using System.Collections.Generic; + +namespace Google.Protobuf.Collections +{ + /// + /// Provides a central place to implement equality comparisons, primarily for bitwise float/double equality. + /// + public static class ProtobufEqualityComparers + { + /// + /// Returns an equality comparer for suitable for Protobuf equality comparisons. + /// This is usually just the default equality comparer for the type, but floating point numbers are compared + /// bitwise. + /// + /// The type of equality comparer to return. + /// The equality comparer. + public static EqualityComparer GetEqualityComparer() + { + return typeof(T) == typeof(double) ? (EqualityComparer) (object) BitwiseDoubleEqualityComparer + : typeof(T) == typeof(float) ? (EqualityComparer) (object) BitwiseSingleEqualityComparer + : typeof(T) == typeof(double?) ? (EqualityComparer) (object) BitwiseNullableDoubleEqualityComparer + : typeof(T) == typeof(float?) ? (EqualityComparer) (object) BitwiseNullableSingleEqualityComparer + : EqualityComparer.Default; + } + + /// + /// Returns an equality comparer suitable for comparing 64-bit floating point values, by bitwise comparison. + /// (NaN values are considered equal, but only when they have the same representation.) + /// + public static EqualityComparer BitwiseDoubleEqualityComparer { get; } = new BitwiseDoubleEqualityComparerImpl(); + + /// + /// Returns an equality comparer suitable for comparing 32-bit floating point values, by bitwise comparison. + /// (NaN values are considered equal, but only when they have the same representation.) + /// + public static EqualityComparer BitwiseSingleEqualityComparer { get; } = new BitwiseSingleEqualityComparerImpl(); + + /// + /// Returns an equality comparer suitable for comparing nullable 64-bit floating point values, by bitwise comparison. + /// (NaN values are considered equal, but only when they have the same representation.) + /// + public static EqualityComparer BitwiseNullableDoubleEqualityComparer { get; } = new BitwiseNullableDoubleEqualityComparerImpl(); + + /// + /// Returns an equality comparer suitable for comparing nullable 32-bit floating point values, by bitwise comparison. + /// (NaN values are considered equal, but only when they have the same representation.) + /// + public static EqualityComparer BitwiseNullableSingleEqualityComparer { get; } = new BitwiseNullableSingleEqualityComparerImpl(); + + private class BitwiseDoubleEqualityComparerImpl : EqualityComparer + { + public override bool Equals(double x, double y) => + BitConverter.DoubleToInt64Bits(x) == BitConverter.DoubleToInt64Bits(y); + + public override int GetHashCode(double obj) => + BitConverter.DoubleToInt64Bits(obj).GetHashCode(); + } + + private class BitwiseSingleEqualityComparerImpl : EqualityComparer + { + // Just promote values to double and use BitConverter.DoubleToInt64Bits, + // as there's no BitConverter.SingleToInt32Bits, unfortunately. + + public override bool Equals(float x, float y) => + BitConverter.DoubleToInt64Bits(x) == BitConverter.DoubleToInt64Bits(y); + + public override int GetHashCode(float obj) => + BitConverter.DoubleToInt64Bits(obj).GetHashCode(); + } + + private class BitwiseNullableDoubleEqualityComparerImpl : EqualityComparer + { + public override bool Equals(double? x, double? y) => + x == null && y == null ? true + : x == null || y == null ? false + : BitwiseDoubleEqualityComparer.Equals(x.Value, y.Value); + + // The hash code for null is just a constant which is at least *unlikely* to be used + // elsewhere. (Compared with 0, say.) + public override int GetHashCode(double? obj) => + obj == null ? 293864 : BitwiseDoubleEqualityComparer.GetHashCode(obj.Value); + } + + private class BitwiseNullableSingleEqualityComparerImpl : EqualityComparer + { + public override bool Equals(float? x, float? y) => + x == null && y == null ? true + : x == null || y == null ? false + : BitwiseSingleEqualityComparer.Equals(x.Value, y.Value); + + // The hash code for null is just a constant which is at least *unlikely* to be used + // elsewhere. (Compared with 0, say.) + public override int GetHashCode(float? obj) => + obj == null ? 293864 : BitwiseSingleEqualityComparer.GetHashCode(obj.Value); + } + } +} diff --git a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs index 6063ff61..c18b63e2 100755 --- a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs +++ b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs @@ -51,6 +51,7 @@ namespace Google.Protobuf.Collections , IReadOnlyList #endif { + private static readonly EqualityComparer EqualityComparer = ProtobufEqualityComparers.GetEqualityComparer(); private static readonly T[] EmptyArray = new T[0]; private const int MinArraySize = 8; @@ -434,7 +435,7 @@ namespace Google.Protobuf.Collections { return false; } - EqualityComparer comparer = EqualityComparer.Default; + EqualityComparer comparer = EqualityComparer; for (int i = 0; i < count; i++) { if (!comparer.Equals(array[i], other.array[i])) @@ -454,7 +455,7 @@ namespace Google.Protobuf.Collections public int IndexOf(T item) { ProtoPreconditions.CheckNotNullUnconstrained(item, nameof(item)); - EqualityComparer comparer = EqualityComparer.Default; + EqualityComparer comparer = EqualityComparer; for (int i = 0; i < count; i++) { if (comparer.Equals(array[i], item)) diff --git a/csharp/src/Google.Protobuf/FieldCodec.cs b/csharp/src/Google.Protobuf/FieldCodec.cs index c28b47e1..a11f2420 100644 --- a/csharp/src/Google.Protobuf/FieldCodec.cs +++ b/csharp/src/Google.Protobuf/FieldCodec.cs @@ -30,6 +30,7 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #endregion +using Google.Protobuf.Collections; using Google.Protobuf.Compatibility; using Google.Protobuf.WellKnownTypes; using System; @@ -346,6 +347,7 @@ namespace Google.Protobuf /// public sealed class FieldCodec { + private static readonly EqualityComparer EqualityComparer = ProtobufEqualityComparers.GetEqualityComparer(); private static readonly T DefaultDefault; // Only non-nullable value types support packing. This is the simplest way of detecting that. private static readonly bool TypeSupportsPacking = default(T) != null; @@ -469,6 +471,6 @@ namespace Google.Protobuf /// public int CalculateSizeWithTag(T value) => IsDefault(value) ? 0 : ValueSizeCalculator(value) + tagSize; - private bool IsDefault(T value) => EqualityComparer.Default.Equals(value, DefaultValue); + private bool IsDefault(T value) => EqualityComparer.Equals(value, DefaultValue); } } -- cgit v1.2.3