简体   繁体   中英

equals() and == in a generic function

I am making a comparer for set operation on various types.

So I have a generic class

 public class Comparer<T, Tid>
...
     public bool Equals(T x, T y)
       {
          var xid = m_idfunc(x);
           var yid = m_idfunc(y);
           return (Tid)xid == (Tid)yid;
       }

Where m_idfunc is a lambda passed in to the Comparer constructor, it is

Func<T,Tid>

I create a comparer with Tid = string. I get in the equals function xid = string1, yid = string2

If string1 and string 2 are the same ("foo" and "foo" say)

xid == yid

yields false

(Tid)xid == (Tid)yid

also yields false (it should not be necessary - I was just getting desperate)

heres my immediate window - paused on the return xid == yid line

yid.GetType() == typeof(string)
true
xid.GetType() == typeof(string)
true
xid==yid
false
(string)xid==(string)yid
true
xid.Equals(yid)
true

Whats going on?

What's interesting about this is that it might just work the way you want it to. Here's an example:

using System;
using System.Text;

namespace ConsoleApplication1 {

    class Program {

        public static void Main()  {
            string myString = "1";
            object objectString = "1";
            string myCopiedString = string.Copy(myString);
            string internedString = string.Intern(myCopiedString);

            Console.WriteLine(myString); //1
            Console.WriteLine(objectString); //1
            Console.WriteLine(myCopiedString); //1
            Console.WriteLine(internedString); //1

            Console.Write(objectString == myString); //true
            Console.Write(objectString == "1"); //true
            Console.Write(objectString == myCopiedString); //!!!FALSE!!!!
            Console.Write(objectString == internedString); //true
            Console.Write(objectString == SomeMethod()); //!!!FALSE!!!
            Console.Write(objectString == SomeOtherMethod()); //true
        }

        public static string SomeMethod() {
            StringBuilder sb = new StringBuilder();
            return sb.Append("1").ToString();
        }

        public static string SomeOtherMethod() {
            return "1".ToString();
        }        
    }
}

The reason why it might work is due to string interning. So, this is definitely one to watch out for, because it can actually work when you test it, but depending on the implementation, it might suddenly break.

In your case, you need to determine whether you care about Reference equality or "value" equality. == is reference equality, which again, depending on whether or not the string is interned may be true. I suspect you actually want to use EqualityComparer<T>.Default.Equals in your function.

If you run open this in VS you'll see the compiler warning: “Possible unintended reference comparison; to get a value comparison, cast the left hand side to type 'string'”. In your case however, the compiler can't warn you, because as far as it knows, the types are objects, it doesn't know that one or both are string.

My initial assumption was that because it's generics, it can't do a reference to value conversion behind the scenes that it does for strings. I wanted to put together an example that supported this. :) I had to make a few assumptions to put something together for this so my example may not be 100% on. (code I used is at the bottom)

I wasn't able to get anything to compile when I just had

class Comparer<T, TId>
{
    private readonly Func<T, TId> m_idfunc;
    public Comparer(Func<T, TId> idFunc)
    {
        m_idfunc = idFunc;
    }

    public bool Equals(T x, T y)
    {
        var xid = m_idfunc(x);
        var yid = m_idfunc(y);
        return (TId)xid == (TId)yid;
    }
}

I found https://stackoverflow.com/a/390919/156708 and modifed the class declaration to be

class Comparer<T, TId> where TId : class 

and it compiled. Step 1.

I set up the Equals function as

public bool Equals(T x, T y)
{
    var xid = m_idfunc(x);
    var yid = m_idfunc(y);
    return (TId)xid == (TId)yid;
}

And the result is False (see full code for generation of value in xid|yid). Fitting my assumption that Generics has a hand in this. Not enough yet, need to see what happens if the Generics aspect is removed.

Changing the Comparer class to be

class Comparer<T>
{
    private readonly Func<T, string> m_idfunc;
    public Comparer(Func<T, string> idFunc)
    {
        m_idfunc = idFunc;
    }

    public bool Equals(T x, T y)
    {
        var xid = m_idfunc(x);
        var yid = m_idfunc(y);
        return xid == yid;
    }
}

returns True .

I'm not 100% on this, but my assumption is based on the fact that the == operator of the string class does a value check instead of a reference check. When using generics, it is likely setting up to only do reference checks (haven't dug into the IL to see what it's doing there), and if the string locations in memory are not the same then it will return false. (I'm kinda winging the details, as I don't know them prezactly, just a working hypothesis that seems to be working)

My complete sample code with generics is below.

using System;
namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var compare = new Comparer<Example, string>(example => example.id(example));
            var ex1 = new Example();
            var ex2 = new Example();
            Console.WriteLine(compare.Equals(ex1, ex2));
            Console.ReadLine();
        }
        class Example
        {
            public string id(Example example)
            {
                return new string(new [] {'f', 'o', 'o'});
            }
        }
        class Comparer<T, TId> where TId : class 
        {
            private readonly Func<T, TId> m_idfunc;
            public Comparer(Func<T, TId> idFunc)
            {
                m_idfunc = idFunc;
            }

            public bool Equals(T x, T y)
            {
                var xid = m_idfunc(x);
                var yid = m_idfunc(y);
                return (TId)xid == (TId)yid;
            }
        }
    }
}

Hope that helps... and that I'm not terribly wrong on my reasoning. :)

I think, it is more correct to use EqualityComparer<TId> inside Comparer<T, Tid> . Besides, instead of delegate I would use interface to get identifiers:

interface IObjectWithId<T>
{
    T Id { get; }
}

class IdEqualityComparer<T, TId> : EqualityComparer<T>
    where T : IObjectWithId<TId>
{
    public override bool Equals(T x, T y)
    {
        return EqualityComparer<TId>.Default.Equals(x.Id, y.Id);
    }

    public override int GetHashCode(T obj)
    {
        return EqualityComparer<TId>.Default.GetHashCode(obj.Id);
    }
}

class A : IObjectWithId<string>
{
    public string Id { get; set; }
}

Usage:

var a = new A { Id = "foo" };
var b = new A { Id = "foo" };
var c = new A { Id = "bar" };

var comparer = new IdEqualityComparer<A, string>();

Console.WriteLine(comparer.Equals(a, b)); // true
Console.WriteLine(comparer.Equals(a, c)); // false

The C operator "==" has two very different meanings. It can either invoke a type-specific overloaded equality-operator method if the compiler can statically determine that such a method is applicable to the operand types, or it can perform a reference comparison between the operands, if both operands are known to be reference types and there may exist an object which could be referred to by both operands. For most types, only one type of comparison would be possible; value types do not support reference comparison, and most reference types do not overload the equality operator. There is, however, a common class which would support both types of comparison: System.String .

The vb.net language avoids ambiguity here by only allowing the = operator to be used on types which overload it. For reference comparisons, the Is operator is required. If one were to attempt to write your code in vb.net, the = operator would not be permitted on class-constrained generics. One could use the Is operator, but it would check for reference equality regardless of whether the operands overload = .

As it is, in C#, assuming you have a class constraint on your generic type (the == operator won't work without it), the compiler can only use an overloaded equality operator on a generic type if the type is constrained to one for which the operator is overloaded. Since you don't constrain your generic type parameter to string (indeed, since string is sealed, the compiler won't allow such a constraint) there's no way the compiler can use the string overload of the equality operator. Thus, it uses the version of the equality operator that it knows is available on a class-constrained generic--reference equality (equivalent to the Is operator in vb.net).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM