简体   繁体   中英

Creating a custom comparer for a complex object of Type T

I am creating a Tree structure in C# and I have some specific goals:

  • When a user of Tree creates new Tree they are required to pass in a type of ITreeComparer , the user knows the type of T and must create the correct Comparer.
  • Tree class creates all instances of TreeNode the caller only adds values of type T to the Tree class. TreeNode is private and a child class of the Tree class So I need a way to pass the IComparer to TreeNode so I made ITreeComparer static.
  • I want to use my custom ITreeComparer for complex types of 'T' but I would like to have ITReeComparer inherit from IComparer so that a user of Tree is not required not required to pass ITreeComparer when the type T is primitives, like double and int.

My questions:

  1. I wish there was a way to keep the ITreeComparer in the Tree class even though Tree class handles comparisons of itself. I made the ITreeComparision property in TreeNode static, any better solutions than this?
  2. I am getting a warning that TreeNode defines '==' or '!=' but does not override 'Object.Equals(object o) and 'Object.GetHasCode' , why would I use Object.Equals when I want to use the _comparer.Compare(x.Value, y.Value) , _comparer is the passed in ITreeComparer instance creator of Tree must supply.

  3. If ITreeComparer were to implement IComparer would I just check if my _comparer is null and if is use the IComparer default Comparison, that way as user of Tree does not have to pass in an ITreeComparison implmentation?

     public interface ITreeComparer<in T> { int Compare(T x, T y); } public class Tree<T> { private TreeNode _root = null; private ITreeComparer<T> _comparer = null; public Tree(ITreeComparer<T> comparer) { _comparer = comparer; } public Tree(T value, ITreeComparer<T> comparer) { _root = new TreeNode(value,_comparer); _comparer = comparer; } public T Add(T value) { var newNode = new TreeNode(value, _comparer); if (_root == null) { _root = newNode; return _root.Value; } else { var startEdgeDirection = EdgeDirection.Left; if (_root > newNode) startEdgeDirection = EdgeDirection.Right; var parent = FindItemNodeParent(value, _root, startEdgeDirection); } return value; } private TreeNode FindItemNodeParent(T value, TreeNode current , EdgeDirection direction) { throw new NotImplementedException(); if (_comparer.Compare(current.Value, value) == 0) return null; if (direction == EdgeDirection.Left) { } else { } } private TreeNode Search(T value, TreeNode current, EdgeDirection direction ) { throw new NotImplementedException(); if (_comparer.Compare(current.Value, value) == 0) return null; if (direction == EdgeDirection.Left) { } else { } } private enum EdgeDirection { Left, Right } private class TreeNode { private static ITreeComparer<T> _comparer; public TreeNode LeftEdge { get; set; } public TreeNode RightEdge { get; set; } public T Value { get; set; } public TreeNode(ITreeComparer<T> comparer) { _comparer = comparer; } public TreeNode(T value, ITreeComparer<T> comparer) { Value = value; _comparer = comparer; } public static bool operator < (TreeNode x, TreeNode y) { if (x == null) return y == null; return _comparer.Compare(x.Value, y.Value) < 0; } public static bool operator > (TreeNode x, TreeNode y) { if (x == null) return y == null; return _comparer.Compare(x.Value, y.Value) > 0; } public static bool operator == (TreeNode x, TreeNode y) { if (x == null) return y == null; return _comparer.Compare(x.Value, y.Value) == 0; } public static bool operator != (TreeNode x, TreeNode y) { if (x == null) return y == null; return _comparer.Compare(x.Value, y.Value) != 0; } } 

    }

UPDATE

After the advice from the helpful Stackoverflowers I am just using IComparable, I have removed all my overloaded comparison operators from TreeNode and I am setting my private IComparer<T> _comparer to Comparer<T>.Default . This always works because I added `where T: IComparable', which means a user of Tree will have to implement IComparable on their custom T object, for C# primitives, IComparable already implemented and they will not have to implement IComparable when T is int for example and they will never need to pass in an IComparer because the must always implement IComparable for their type T.

    public partial class Tree<T> where T : IComparable<T>
{
    private TreeNode _root = null;
    private IComparer<T> _comparer = null;


    public Tree()
    {
        _comparer = Comparer<T>.Default;
    }

    public Tree(T value)
    {
        _root = new TreeNode(value);
        _comparer = Comparer<T>.Default;
    }

    public T Add(T value)
    {
        var newNode = new TreeNode(value);
        if (_root == null)
        {
            _root = newNode;
            return _root.Value;
        }

        var startEdgeDirection = EdgeDirection.Left;

        if (_comparer.Compare(_root.Value, value) > 0)
            startEdgeDirection = EdgeDirection.Right;

        var parent = FindItemNodeParent(value, _root, startEdgeDirection);

        if (parent != null)
        {
            if (_comparer.Compare(parent.Value, value) > 0)
            {
                parent.RightDescendant = newNode;
            }
            else
            {
                parent.LeftDescendant = newNode;
            }
        }


        return value;
    }

    private TreeNode FindItemNodeParent(T value, TreeNode current, EdgeDirection direction)
    {
        if (_comparer.Compare(current.Value, value) == 0)
            return null;

        if (direction == EdgeDirection.Left)
        {
            if (current.LeftDescendant == null)
                return current;

            if (_comparer.Compare(current.LeftDescendant.Value, value) > 0)
            {
                FindItemNodeParent(value, current.LeftDescendant, EdgeDirection.Right);
            }
            else
            {
                FindItemNodeParent(value, current.LeftDescendant, EdgeDirection.Left);
            }
        }
        else
        {
            if (current.RightDescendant == null)
                return current;

            if (_comparer.Compare(current.RightDescendant.Value, value) > 0)
            {
                FindItemNodeParent(value, current.RightDescendant, EdgeDirection.Right);
            }
            else
            {
                FindItemNodeParent(value, current.RightDescendant, EdgeDirection.Left);
            }

        }

        return null;

    }

    private TreeNode Search(T value, TreeNode current, EdgeDirection direction)
    {
        throw new NotImplementedException();

        if (_comparer.Compare(current.Value, value) == 0)
            return null;

        if (direction == EdgeDirection.Left)
        {

        }
        else
        {

        }
    }

    private enum EdgeDirection
    {
        Left,
        Right
    }

}

 public partial class Tree<T>
{
    private class TreeNode
    {
        public TreeNode LeftDescendant { get; set; }
        public TreeNode RightDescendant { get; set; }
        public T Value { get; set; }

        public TreeNode(T value)
        {
            Value = value;
        }
    }
}

As I mentioned in my comment above, I don't understand why you have the interface ITreeComparer . It seems to me that it's exactly the same as IComparer , and so you could just use IComparer instead.

That said…

  1. I wish there was a way to keep the ITreeComparer in the Tree class even though Tree class handles comparisons of itself. I made the ITreeComparision property in TreeNode static, any better solutions than this?

I agree making it static is a bad idea. One obvious problem is that it means you are limited to only a single kind of Tree<T> in a process (ie for any given T , there can only ever be one kind of comparison). This means, for example, that if you have a class with properties Name and Id , you can only have trees that order by Name , or trees that order by Id , but not both kinds of trees a the same time.

Indeed, with this field static in the TreeNode<T> class, there's no point in it being an instance field in Tree<T> , since all Tree<T> objects are going to use the same TreeNode<T> type.

It seems to me that the only reason you even expose the ITreeComparer to the TreeNode is for the purpose of allowing the overloaded comparison operators. If you must have those operators, I would go ahead and make the _comparer field non-static in TreeNode<T> , or even just change it to a reference to the parent Tree<T> object (and then it can get the _comparer from the parent).

But really, I don't feel that the comparison operators help much. You might as well just call _comparer directly in the Tree<T> class and not bother having TreeNode<T> even know how to compare itself.

  1. I am getting a warning that TreeNode defines '==' or '!=' but does not override 'Object.Equals(object o) and 'Object.GetHasCode', why would I use Object.Equals when I want to use the _comparer.Compare(x.Value, y.Value), _comparer is the passed in ITreeComparer instance creator of Tree must supply.

For better or worse, there are several different ways for objects to compare themselves in C#. Any time these ways are not implemented identically, you are setting yourself up for some real head-scratcher bugs.

So, when you overload equality-related operators == and != , but not the Equals() method, you get a warning. Likewise, implementing custom equality relationships without fixing GetHashCode() to properly reflect those equality relationships is a good way to get very-hard-to-fix bugs.

It's really important, if you start implementing equality operations within a type itself, to make sure you go through and make consistent all equality-related operations in that type.

  1. If ITreeComparer were to implement IComparer would I just check if my _comparer is null and if is use the IComparer default Comparison, that way as user of Tree does not have to pass in an ITreeComparison implmentation?

As I mentioned, I think you should just drop ITreeComparer altogether. There's no actual "sandboxing" going on…indeed, the user could make a single class implement both interfaces if they wanted, using the exact same method within the interface. Forcing a user to implement a custom interface when a built-in one would suffice just makes things more annoying for them.


As long as I'm writing, I'll agree that the non-answer provided by ipavlu does offer some useful observations. In particular, your handling of nulls is all wrong. The other post notes some of the problems. Another problem is if x and y are both null, your comparison operators both return true . Ie according to the code, x and y are both simultaneously greater than and less than each other, if they are both null.

Fortunately, there's really no apparent reason that the comparisons should need to handle nulls at all. The original implementation (provided by the user) is by convention required to accommodate nulls already. Your own code should not need to check for them. Furthermore, there's no apparent cause for null values to appear in the tree at all. A null node reference doesn't make much sense: any time you reach a null in your tree, that's where you're going to store a node; there's no need to decide whether the new node goes to the left or to the right of that null…it goes right where the null is at that time!

There is big problem in your code:

public static bool operator == (TreeNode x, TreeNode y)
{
    if (x == null) return y == null;

    return _comparer.Compare(x.Value, y.Value) == 0;
}

x is TreeNode and y is TreeNode, so:

      if (x == null) return y == null;

x == null would end up calling operator== method, so the method is calling itself.

You have to do this:

(object)x == null, 

As long as you use the same type, the operator== will call itself. For this to make it clean is here object.ReferenceEquals.

Also, did you noted, that your code is not null safe? What it x != null and y == null? Then y.Value will throw exception in line:

    return _comparer.Compare(x.Value, y.Value) == 0;

Try something like this instead:

public static bool operator == (TreeNode x, TreeNode y)
{
    var x_null = object.ReferenceEquals(x,null);
    var y_null = object.ReferenceEquals(y,null);
    if (x_null && y_null) return true;
    if (x_null || y_null) return false;

    //now is x.Value, y.Value is safe to call
    return _comparer.Compare(x.Value, y.Value) == 0;
}

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