Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

Time:2020-9-29

1: Background

1. Tell a story

Zeng Jin found that a colleague did not rewrite the equals method when he found that he had defined the structure. For example, the following code:


    static void Main(string[] args)
    {
        var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();
        var item = list.FirstOrDefault(m => m.Equals(new Point(int.MaxValue, int.MaxValue)));
        Console.ReadLine();
    }

    public struct Point
    {
        public int x;
        public int y;

        public Point(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
    }

This code seems to have no problem, as if we usually write this way, it doesn’t matter, if there is any problem, run and then WinDbg to have a look.

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble


0:000> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ff8826fba20       10        16592 ConsoleApp6.Point[]
00007ff8e0055e70        6        35448 System.Object[]
00007ff8826f5b50     2000        48000 ConsoleApp6.Point

0:000> !dumpheap  -mt 00007ff8826f5b50
         Address               MT     Size
0000020d00006fe0 00007ff8826f5b50       24     

0:000> !do 0000020d00006fe0
Name:        ConsoleApp6.Point
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ff8e00585a0  4000001        8         System.Int32  1 instance                0 x
00007ff8e00585a0  4000002        c         System.Int32  1 instance                0 y

Do you see the problem from the output above? There are 2000 points on the managed heap, and they can be used!doType it to show that these are reference types… Where do these reference types come from? The code should beequalsWhen comparing, two points will be boxed and put on the managed heap in one comparison. This is a terrible situation, and we should know that the reference object itself has(8+8) byteIt’s a huge waste of time and space…

2: Explore the default equals implementation

1. Find the equals implementation of ValueType

Why is this? We knowequalsIs inherited fromValueTypeYes, soValueTypeIf you look at it, you can see:


    public abstract class ValueType
    {
        public override bool Equals(object obj)
        {
            if (CanCompareBits(this)) {return FastEqualsCheck(this, obj);}
            FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            for (int i = 0; i < fields.Length; i++)
            {
                object obj2 = ((RtFieldInfo)fields[i]).UnsafeGetValue(this);
                object obj3 = ((RtFieldInfo)fields[i]).UnsafeGetValue(obj);
                ...
            }
            return true;
        }
    }

From the above code, we can see the following three information:

<1> GenericequalsMethod receives the object type, and the parameters are boxed once.

<2> CanCompareBits,FastEqualsCheckThey all use the object type,thisIt also needs to be packed once.

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

<3> There are two ways to compare, or useFastEqualsCheckCompare or adoptreflexComparison, I go to… Reflection plays big.

Overall, it’s OK,equalsBoth objects of comparison are boxed.

2. Improvement plan

If the problem is found, it will be easy to solve. If I don’t use the general equals method, I will customize an equals method and run the code.

        public bool Equals(Point other)
        {
            return this.x == other.x && this.y == other.y;
        }

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

You can see that I left my custom equals,. It seems that the problem has been solved simply and roughly. I’m so happy. The moment of slapping begins…

3: Did you really solve the problem?

1. Problems encountered

Most of the time, we will define various generic classes. In the generic operation, we usually involve the equals between T. for example, the following code I designed. For convenience, I set thePointAlso rewrite the default equals of.

class Program
    {
        static void Main(string[] args)
        {

            var p1 = new Point(1, 1);
            var p2 = new Point(1, 1);

            TProxy<Point> proxy = new TProxy<Point>() { Instance = p1 };

            Console.WriteLine($"p1==p2 {proxy.IsEquals(p2)}");
            Console.ReadLine();
        }
    }

    public struct Point
    {
        public int x;
        public int y;

        public Point(int x, int y)
        {
            this.x = x;
            this.y = y;
        }

        public override bool Equals(object obj)
        {
            Console.WriteLine ("I am GM's equals");
            return base.Equals(obj);
        }

        public bool Equals(Point other)
        {
            Console.WriteLine ("I am custom equals");
            return this.x == other.x && this.y == other.y;
        }
    }

    public class TProxy<T>
    {
        public T Instance { get; set; }

        public bool IsEquals(T obj)
        {
            var b = Instance.Equals(obj);

            return b;
        }
    }

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

From the output result, we still use the general equals method, which is embarrassing. Why is this?

2. Find the problem from the value type implementation of FCL

Sometimes, if you can’t find a problem by hard thinking, suddenly, there are some custom value types in FCL? such asint,long,decimalWhy don’t you look at how they work, look for inspiration, right… Do what you say, do itint32Source code.


public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>
{
     public override bool Equals(object obj)
    {
        if (!(obj is int))
        {
            return false;
        }
        return this == (int)obj;
    }

    public bool Equals(int obj)
    {
        return this == obj;
    }
}

I’m still int. it seems that my point has less interface implementation than int. this is the problem, and the last generic interfaceIEquatable<int>In particular, let’s look at the definition:


public interface IEquatable<T>
{
    bool Equals(T other);
}

There is only one generic interfaceequalsBut inspiration tells me, it seems… Maybe… should… This is the generic oneequalsIs used to solve the generic caseequalsComparison.

3. Add iequatable < T > interface

With this idea, I also learn from FCL and let point realizeIEquatable<T>Interface, and then in theTProxy<T>Must be implemented under constraints in proxy classesIEquatable<T>The modification code is as follows:


    public struct Point : IEquatable<Point> { ...  }
    public class TProxy<T> where T: IEquatable<T> { ... }

Then run the program, as shown in the following figure:

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

Although it is a success, there is one place that I am not very comfortable with, that is, the second line of code above, in theTProxy<T>It’s a constraintTBecause I look at itListThe implementation of this generic constraint does not do ah, it may be a bit obsessive-compulsive, paste the code for you to see.


public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
{}

Then I continued to imitate list and putTProxy<T>If you remove the T constraint on, you’ll have a problem and return to itGeneral equals

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

4. Find the answer from the contains source code of list

Curiosity again drove me to find out how to do it in the list. In order to see the native methods in the list, I modified the code as follows, fromContainsMethods.

var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();
    var item = list.Contains(new Point(int.MaxValue, int.MaxValue));

---------- outout ---------------
I am a custom equals
I am a custom equals
I am a custom equals
...

I’m too curious. Take a lookContainsSource code, simplified implementation as follows.


public bool Contains(T item)
{
    ...
    EqualityComparer<T> @default = EqualityComparer<T>.Default;
    for (int j = 0; j < _size; j++)
    {
        if (@default.Equals(_items[j], item)) {return true;}
    }
    return false;
}

It turns out that the list is in progressequalsBefore comparing, I built a generic comparerEqualityComparer<T>, and then go on chasing the code.

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

Because it’s hereruntimeTypeYesIEquatable<T>Interface, so the code returns a generic comparator:GenericEqualityComparer<T>And then we’ll go on to see what the generic comparer looks like.

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

As you can see from the figure, it’s still trueTYesIEquatable<T>However, the constraints are extracted here, which are quite powerful. Then I also learn to imitate them:

Do not forget to override equals for custom value types, otherwise both performance and space will be in trouble

You can see also left my custom implementation, two ways you can use ha.

The last thing to note is when you rewrite itEqualsAfter that, the compiler will tell you that you’d betterGetHashCodeRewrite it. It’s just suggested that if you don’t like this prompt, customize it as much as possibleGetHashCodeMethods lethashcodeA little more uniform distribution.

4: Summary

Be sure to implement theEqualsMethod, other people’sEqualsMethod is used to cover the bottom, a comparison of two packing, but your program is double kill oh.

Recommended Today

PHP clear HTML code, space, carriage return newline function

Copy codeThe code is as follows: function DeleteHtml($str) { $str = trim($str); $str = strip_tags($str,””); $str = ereg_replace(“\t”,””,$str); $str = ereg_replace(“\r\n”,””,$str); $str = ereg_replace(“\r”,””,$str); $str = ereg_replace(“\n”,””,$str); $str = ereg_replace(“&nbsp;”,” “,$str); return trim($str); } This function can be extended and modified as needed.