How to avoid reference comparison with self implemented value types

I'm trying to implement a value type that in general mimics the behavior of type short.

So far comparison and assignments between my value type and short are working out fine, but when boxing jumps in the problems starts.

Below you can find a unit tests illustrating the problem as well as the source for my value type.

The first assert method uses the overriden Equals method of my value type, the second assert actually result in a reference comparison (RuntimeHelpers.Equals(this, obj);) which of course fails.

The test:

[TestMethod]
public void EqualsTest()
{
    const short SHORT_TYPE = 1;
    MyValueType valueType = SHORT_TYPE;
    object shortObject = SHORT_TYPE;
    object valueObject = valueType;

    Assert.IsTrue(valueObject.Equals(shortObject)); // success
    Assert.IsTrue(shortObject.Equals(valueObject)); // failed, comparing 2 references
}

The value type:

[ComVisible(true)]
[Serializable]
[StructLayout(LayoutKind.Sequential)]
[DebuggerDisplay("{_Value}")]
public struct MyValueType : IComparable, IFormattable, IConvertible, IComparable<short>, IEquatable<short>, IEquatable<MyValueType>
{
    readonly short _Value;

    public int CompareTo(Object value)
    {
        if (value == null)
            return 1;

        if (value is MyValueType)
            return _Value - ((MyValueType)value)._Value;

        if (value is short)
            return _Value - ((short)value);

        throw new ArgumentException("argument must be MyValueType");
    }

    public int CompareTo(short value) { return _Value - value; }
    public int CompareTo(MyValueType myValue) { return _Value - myValue._Value; }
    public override bool Equals(Object obj) { return _Value == obj as short? || _Value == obj as MyValueType?; }
    public bool Equals(MyValueType obj) { return _Value == obj._Value; }
    public bool Equals(short obj) { return _Value == obj; }
    public override int GetHashCode() { return ((ushort)_Value | (_Value << 16)); }

    [SecuritySafeCritical]
    public override String ToString() { return _Value.ToString(); }

    [SecuritySafeCritical]
    public String ToString(IFormatProvider provider) { return _Value.ToString(provider); }

    public String ToString(String format) { return ToString(format, NumberFormatInfo.CurrentInfo); }
    public String ToString(String format, IFormatProvider provider) { return ToString(format, NumberFormatInfo.GetInstance(provider)); }

    [SecuritySafeCritical]
    String ToString(String format, NumberFormatInfo info) { return _Value.ToString(format, info); }

    public static MyValueType Parse(String s) { return Parse(s, NumberStyles.Integer, NumberFormatInfo.CurrentInfo); }
    public static MyValueType Parse(String s, NumberStyles style) { return short.Parse(s, style); }
    public static MyValueType Parse(String s, IFormatProvider provider) { return Parse(s, NumberStyles.Integer, NumberFormatInfo.GetInstance(provider)); }
    public static MyValueType Parse(String s, NumberStyles style, IFormatProvider provider) { return short.Parse(s, style, provider); }
    static MyValueType Parse(String s, NumberStyles style, NumberFormatInfo info) { return short.Parse(s, style, info); }
    public TypeCode GetTypeCode() { return TypeCode.Int16; }
    bool IConvertible.ToBoolean(IFormatProvider provider) { return Convert.ToBoolean(_Value); }
    char IConvertible.ToChar(IFormatProvider provider) { return Convert.ToChar(_Value); }
    sbyte IConvertible.ToSByte(IFormatProvider provider) { return Convert.ToSByte(_Value); }
    byte IConvertible.ToByte(IFormatProvider provider) { return Convert.ToByte(_Value); }
    short IConvertible.ToInt16(IFormatProvider provider) { return _Value; }
    ushort IConvertible.ToUInt16(IFormatProvider provider) { return Convert.ToUInt16(_Value); }
    int IConvertible.ToInt32(IFormatProvider provider) { return Convert.ToInt32(_Value); }
    uint IConvertible.ToUInt32(IFormatProvider provider) { return Convert.ToUInt32(_Value); }
    long IConvertible.ToInt64(IFormatProvider provider) { return Convert.ToInt64(_Value); }
    ulong IConvertible.ToUInt64(IFormatProvider provider) { return Convert.ToUInt64(_Value); }
    float IConvertible.ToSingle(IFormatProvider provider) { return Convert.ToSingle(_Value); }
    double IConvertible.ToDouble(IFormatProvider provider) { return Convert.ToDouble(_Value); }
    Decimal IConvertible.ToDecimal(IFormatProvider provider) { return Convert.ToDecimal(_Value); }
    DateTime IConvertible.ToDateTime(IFormatProvider provider) { throw new InvalidCastException(); }
    Object IConvertible.ToType(Type type, IFormatProvider provider) { throw new NotImplementedException(); }
    bool IEquatable<short>.Equals(short other) { return _Value.Equals(other); }

    public MyValueType(short value) { _Value = value; }

    public static implicit operator MyValueType(short value) { return new MyValueType(value); }
    public static implicit operator short(MyValueType myValueType) { return myValueType._Value; }

    //public static explicit operator MyValueType(short value) { return new MyValueType(value); }
    //public static explicit operator short(MyValueType myValueType) { return myValueType; }
    //public static bool operator ==(MyValueType first, MyValueType second) { return first._Value == second._Value; }
    //public static bool operator !=(MyValueType first, MyValueType second) { return first._Value != second._Value; }
}

Edit: added complete lists of unit tests, failing asserts are at bottom and marked

[TestClass]
public class MyValueTypeTest
{
    [TestMethod]
    public void AssignShortTest()
    {
        MyValueType valueType = 1;
        short shortType = valueType;
        Assert.IsTrue(shortType == 1);
    }

    [TestMethod]
    public void AssignValueTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = SHORT_TYPE;
        Assert.IsTrue(valueType == 1);
    }

    [TestMethod]
    public void DictionaryShortTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = 1;
        Dictionary<short, string> dict = new Dictionary<short, string>();
        dict.Add(SHORT_TYPE, "short");
        Assert.IsTrue(dict.ContainsKey(valueType));
    }

    [TestMethod]
    public void DictionaryValueTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = 1;
        Dictionary<MyValueType, string> dict = new Dictionary<MyValueType, string>();
        dict.Add(valueType, "value");
        Assert.IsTrue(dict.ContainsKey(SHORT_TYPE));
    }

    [TestMethod]
    public void EqualsOperatorTest()
    {
        const short SHORT_TYPE_A = 1;
        MyValueType valueTypeA = 1;
        MyValueType valueTypeB = 1;

        Assert.IsTrue(valueTypeA == valueTypeB);
        Assert.IsTrue(SHORT_TYPE_A == valueTypeA);
        Assert.IsTrue(valueTypeA == SHORT_TYPE_A);
    }

    [TestMethod]
    public void ShortEqualsTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = 1;
        Assert.IsTrue(SHORT_TYPE.Equals(valueType));
    }

    public static bool Test(Object objA, Object objB)
    {
        if (objA == objB)
            return true;
        if (objA == null || objB == null)
            return false;
        return objA.Equals(objB);
    }

    [TestMethod]
    public void ValueEqualsTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = 1;
        Assert.IsTrue(valueType.Equals(SHORT_TYPE));
    }

    [TestMethod]
    public void AreEqualTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = 1;
        Assert.AreEqual(valueType, SHORT_TYPE, "test 1"); // success
        Assert.AreEqual(SHORT_TYPE, valueType, "test 2"); // failed, comparing 2 references
    }

    [TestMethod]
    public void ObjectEqualsTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = 1;
        Assert.IsTrue(object.Equals(valueType, SHORT_TYPE), "test 1"); // success
        Assert.IsTrue(object.Equals(SHORT_TYPE, valueType), "test 2"); // failed, comparing 2 references
    }

    [TestMethod]
    public void EqualsTest()
    {
        const short SHORT_TYPE = 1;
        MyValueType valueType = SHORT_TYPE;
        object shortObject = SHORT_TYPE;
        object valueObject = valueType;

        Assert.IsTrue(valueObject.Equals(shortObject), "test 1"); // success
        Assert.IsTrue(shortObject.Equals(valueObject), "test 2"); // failed,    comparing 2 references
    }
}
Jon Skeet
people
quotationmark

You can't do this. Basically, there's no way of symmetrically implementing Equals between two types unless they know about each other.

Note that this assertion:

Assert.IsTrue(SHORT_TYPE.Equals(valueType));

works due to the implicit conversion. It's effectively using:

short converted = valueType;
Assert.IsTrue(SHORT_TYPE.Equals(converted));

That's not the same as your failing assertion, which is passing the boxed MyValueType to Int16.Equals(object).

I doubt that it's performing a reference comparison - it will be calling Int16.Equals(object) which will return false.

Fundamentally, I would give up on this hope - your type is definitely oddly-designed, in terms of implementing IEquatable<short> but not IEquatable<MyValueType> etc. This will behave in a way which is unexpected for many developers. You should revisit the whole design.

people

See more on this question at Stackoverflow