Browse Source

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.
Jon Skeet 8 năm trước cách đây
mục cha
commit
f3e9a65d75

+ 3 - 0
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         \

+ 43 - 0
csharp/src/Google.Protobuf.Test/Collections/MapFieldTest.cs

@@ -540,6 +540,49 @@ namespace Google.Protobuf.Collections
             Assert.Throws<ArgumentException>(() => map.ToString());
         }
 
+        [Test]
+        public void NaNValuesComparedBitwise()
+        {
+            var map1 = new MapField<string, double>
+            {
+                { "x", SampleNaNs.Regular },
+                { "y", SampleNaNs.SignallingFlipped }
+            };
+
+            var map2 = new MapField<string, double>
+            {
+                { "x", SampleNaNs.Regular },
+                { "y", SampleNaNs.PayloadFlipped }
+            };
+
+            var map3 = new MapField<string, double>
+            {
+                { "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<double, string>
+            {
+                { 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()

+ 124 - 0
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<object>.Default, GetEqualityComparer<object>());
+            Assert.AreSame(EqualityComparer<string>.Default, GetEqualityComparer<string>());
+            Assert.AreSame(EqualityComparer<int>.Default, GetEqualityComparer<int>());
+            Assert.AreSame(EqualityComparer<int?>.Default, GetEqualityComparer<int?>());
+        }
+
+        [Test]
+        public void GetEqualityComparer_NotDefault()
+        {
+            // It's more pain than it's worth to try to parameterize these tests.
+            Assert.AreSame(BitwiseDoubleEqualityComparer, GetEqualityComparer<double>());
+            Assert.AreSame(BitwiseSingleEqualityComparer, GetEqualityComparer<float>());
+            Assert.AreSame(BitwiseNullableDoubleEqualityComparer, GetEqualityComparer<double?>());
+            Assert.AreSame(BitwiseNullableSingleEqualityComparer, GetEqualityComparer<float?>());
+        }
+
+        [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<T>(EqualityComparer<T> comparer, IEnumerable<T> 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]);
+                    }
+                }
+            }
+        }
+    }    
+}

+ 13 - 0
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<double> { SampleNaNs.Regular, SampleNaNs.SignallingFlipped };
+            var list2 = new RepeatedField<double> { SampleNaNs.Regular, SampleNaNs.PayloadFlipped };
+            var list3 = new RepeatedField<double> { SampleNaNs.Regular, SampleNaNs.SignallingFlipped };
+
+            EqualityTester.AssertInequality(list1, list2);
+            EqualityTester.AssertEquality(list1, list3);
+            Assert.True(list1.Contains(SampleNaNs.SignallingFlipped));
+            Assert.False(list2.Contains(SampleNaNs.SignallingFlipped));
+        }
     }
 }

+ 11 - 0
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);
+        }
     }
 }

+ 53 - 0
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
+{
+    /// <summary>
+    /// Samples of different not-a-number values, for testing equality comparisons.
+    /// </summary>
+    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);
+    }
+}

+ 11 - 0
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);
+        }
     }
 }

+ 11 - 11
csharp/src/Google.Protobuf/Collections/MapField.cs

@@ -71,9 +71,12 @@ namespace Google.Protobuf.Collections
         , IReadOnlyDictionary<TKey, TValue>
 #endif
     {
+        private static readonly EqualityComparer<TValue> ValueEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<TValue>();
+        private static readonly EqualityComparer<TKey> KeyEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<TKey>();
+
         // TODO: Don't create the map/list until we have an entry. (Assume many maps will be empty.)
         private readonly Dictionary<TKey, LinkedListNode<KeyValuePair<TKey, TValue>>> map =
-            new Dictionary<TKey, LinkedListNode<KeyValuePair<TKey, TValue>>>();
+            new Dictionary<TKey, LinkedListNode<KeyValuePair<TKey, TValue>>>(KeyEqualityComparer);
         private readonly LinkedList<KeyValuePair<TKey, TValue>> list = new LinkedList<KeyValuePair<TKey, TValue>>();
 
         /// <summary>
@@ -131,11 +134,8 @@ namespace Google.Protobuf.Collections
             return map.ContainsKey(key);
         }
 
-        private bool ContainsValue(TValue value)
-        {
-            var comparer = EqualityComparer<TValue>.Default;
-            return list.Any(pair => comparer.Equals(pair.Value, value));
-        }
+        private bool ContainsValue(TValue value) =>
+            list.Any(pair => ValueEqualityComparer.Equals(pair.Value, value));
 
         /// <summary>
         /// Removes the entry identified by the given key from the map.
@@ -293,8 +293,7 @@ namespace Google.Protobuf.Collections
         bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
         {
             TValue value;
-            return TryGetValue(item.Key, out value)
-                && EqualityComparer<TValue>.Default.Equals(item.Value, value);
+            return TryGetValue(item.Key, out value) && ValueEqualityComparer.Equals(item.Value, value);
         }
 
         /// <summary>
@@ -363,11 +362,12 @@ namespace Google.Protobuf.Collections
         /// </returns>
         public override int GetHashCode()
         {
-            var valueComparer = EqualityComparer<TValue>.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<TValue>.Default;
+            var valueComparer = ValueEqualityComparer;
             foreach (var pair in this)
             {
                 TValue value;

+ 130 - 0
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
+{
+    /// <summary>
+    /// Provides a central place to implement equality comparisons, primarily for bitwise float/double equality.
+    /// </summary>
+    public static class ProtobufEqualityComparers
+    {
+        /// <summary>
+        /// Returns an equality comparer for <typeparamref name="T"/> suitable for Protobuf equality comparisons.
+        /// This is usually just the default equality comparer for the type, but floating point numbers are compared
+        /// bitwise.
+        /// </summary>
+        /// <typeparam name="T">The type of equality comparer to return.</typeparam>
+        /// <returns>The equality comparer.</returns>
+        public static EqualityComparer<T> GetEqualityComparer<T>()
+        {
+            return typeof(T) == typeof(double) ? (EqualityComparer<T>) (object) BitwiseDoubleEqualityComparer
+                : typeof(T) == typeof(float) ? (EqualityComparer<T>) (object) BitwiseSingleEqualityComparer
+                : typeof(T) == typeof(double?) ? (EqualityComparer<T>) (object) BitwiseNullableDoubleEqualityComparer
+                : typeof(T) == typeof(float?) ? (EqualityComparer<T>) (object) BitwiseNullableSingleEqualityComparer
+                : EqualityComparer<T>.Default;
+        }
+
+        /// <summary>
+        /// 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.)
+        /// </summary>
+        public static EqualityComparer<double> BitwiseDoubleEqualityComparer { get; } = new BitwiseDoubleEqualityComparerImpl();
+
+        /// <summary>
+        /// 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.)
+        /// </summary>
+        public static EqualityComparer<float> BitwiseSingleEqualityComparer { get; } = new BitwiseSingleEqualityComparerImpl();
+
+        /// <summary>
+        /// 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.)
+        /// </summary>
+        public static EqualityComparer<double?> BitwiseNullableDoubleEqualityComparer { get; } = new BitwiseNullableDoubleEqualityComparerImpl();
+
+        /// <summary>
+        /// 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.)
+        /// </summary>
+        public static EqualityComparer<float?> BitwiseNullableSingleEqualityComparer { get; } = new BitwiseNullableSingleEqualityComparerImpl();
+
+        private class BitwiseDoubleEqualityComparerImpl : EqualityComparer<double>
+        {
+            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<float>
+        {
+            // 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<double?>
+        {
+            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<float?>
+        {
+            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);
+        }
+    }
+}

+ 3 - 2
csharp/src/Google.Protobuf/Collections/RepeatedField.cs

@@ -51,6 +51,7 @@ namespace Google.Protobuf.Collections
         , IReadOnlyList<T>
 #endif
     {
+        private static readonly EqualityComparer<T> EqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<T>();
         private static readonly T[] EmptyArray = new T[0];
         private const int MinArraySize = 8;
 
@@ -434,7 +435,7 @@ namespace Google.Protobuf.Collections
             {
                 return false;
             }
-            EqualityComparer<T> comparer = EqualityComparer<T>.Default;
+            EqualityComparer<T> 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<T> comparer = EqualityComparer<T>.Default;
+            EqualityComparer<T> comparer = EqualityComparer;
             for (int i = 0; i < count; i++)
             {
                 if (comparer.Equals(array[i], item))

+ 3 - 1
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
     /// </remarks>
     public sealed class FieldCodec<T>
     {
+        private static readonly EqualityComparer<T> EqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<T>();
         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
         /// </summary>
         public int CalculateSizeWithTag(T value) => IsDefault(value) ? 0 : ValueSizeCalculator(value) + tagSize;
 
-        private bool IsDefault(T value) => EqualityComparer<T>.Default.Equals(value, DefaultValue);
+        private bool IsDefault(T value) => EqualityComparer.Equals(value, DefaultValue);
     }
 }