IStructuralEquatable和IStructuralComparable解决了什么问题?

68

我注意到在 .NET 4 中添加了这两个接口,以及一些相关的类。它们对我来说似乎有点多余;我读了几篇博客,但仍然无法弄清楚它们解决的问题在 .NET 4 之前是棘手的什么。

IStructuralEquatableIStructuralComparable 有什么用处?

6个回答

54

.NET中的所有类型都支持Object.Equals()方法,该方法默认比较两个类型的引用相等性。然而,有时也希望能够比较两个类型的结构相等性

最好的例子是数组,使用.NET 4现在实现了IStructuralEquatable接口。这使得可以区分是否将两个数组进行引用相等性比较或者“结构相等性”比较——即它们是否在每个位置具有相同值的相同数量的项。下面是一个示例:

int[] array1 = new int[] { 1, 5, 9 };
int[] array2 = new int[] { 1, 5, 9 };

// using reference comparison...
Console.WriteLine( array1.Equals( array2 ) ); // outputs false

// now using the System.Array implementation of IStructuralEquatable
Console.WriteLine(
    StructuralComparisons.StructuralEqualityComparer.Equals( array1, array2 )
); // outputs true

还有其他实现结构相等性/可比较性的类型,如元组和匿名类型 - 它们都明显受益于能够根据结构和内容执行比较的能力。

你没有提到的一个问题是:

为什么我们有IStructuralComparableIStructuralEquatable,当已经存在IComparableIEquatable接口呢?

我可以给出的答案是,一般来说,区分引用比较和结构比较是可取的。通常期望如果你实现了IEquatable<T>.Equals,也会重写Object.Equals以保持一致性。在这种情况下,你如何支持引用和结构相等性?


8
为什么你不能自己指定一个执行这个操作的 IEqualityComparer 呢?IStructuralEquatable 接口对此有什么帮助? - thecoop
1
@thecoop:有两个原因。首先,并非所有类型都实现了接受IEqualityComparerEquals重载-数组就是一个例子,如果我没记错的话。其次,提供相等比较器很好,但如果您想表达某个方法需要可以结构化比较的两个对象的事实怎么办?在这种情况下能够指定IStructuralEquatable/IStructuralComparable实际上是有用的。在任何想应用此类比较的地方传递TupleComparerArrayComparer也是不方便的。这两种方法并不是互斥的。 - LBushkin
1
很遗憾,.NET没有更好地定义相等性,并将两种Equals / GetHashCode类型合并到框架中,其中X.EquivalentTo(Y)表示由X引用的对象的所有成员应该与由Y引用的对象的所有成员行为等效,而X.ValueEquals(Y)表示同时交换所有XY的引用不会影响任何一个成员的行为,除了与等价相关的哈希码。请注意,这两个定义都可以评估任何类型的对象。请注意,基本的Object.EquivalentTo... - supercat
1
应该测试引用相等性;基础的 Object.ValueEquals 应该返回 True [交换类型为 System.Object 的两个实例 XY所有 引用对任何成员除了相关的等价关系 GetHashCode 都没有可观察的影响],但是其状态与其他对象交互的实体应该重写它以测试引用相等性。 - supercat
9
我相当确定这个答案(以及评论)是不准确的。.NET确实支持两种不同版本的相等性:object.Equalsobject.ReferenceEqualsEquals应该被覆盖,以进行任何对于给定类型最有意义的比较,而ReferenceEquals则不能被覆盖,并且总是按引用比较。 - Zenexer
显示剩余6条评论

23

我有同样的问题。 当我运行LBushkin的示例时,我惊讶地发现我得到了不同的答案!即使那个答案有8个赞,它也是错误的。 经过很多的“反射”,这是我的理解。

某些容器(数组、元组、匿名类型)支持IStructuralComparableIStructuralEquatable

  • IStructuralComparable支持深度默认排序。
  • IStructuralEquatable支持深度默认哈希。

{请注意,EqualityComparer<T>支持浅层(仅1个容器级别),默认哈希。}

就我所见,这只在StructuralComparisons类中公开。 我能够想出的唯一有用的方法是创建一个StructuralEqualityComparer<T>辅助类,如下所示:

    public class StructuralEqualityComparer<T> : IEqualityComparer<T>
    {
        public bool Equals(T x, T y)
        {
            return StructuralComparisons.StructuralEqualityComparer.Equals(x,y);
        }

        public int GetHashCode(T obj)
        {
            return StructuralComparisons.StructuralEqualityComparer.GetHashCode(obj);
        }

        private static StructuralEqualityComparer<T> defaultComparer;
        public static StructuralEqualityComparer<T> Default
        {
            get
            {
                StructuralEqualityComparer<T> comparer = defaultComparer;
                if (comparer == null)
                {
                    comparer = new StructuralEqualityComparer<T>();
                    defaultComparer = comparer;
                }
                return comparer;
            }
        }
    }

现在我们可以创建一个HashSet,其中的项具有嵌套在容器内的容器。
        var item1 = Tuple.Create(1, new int[][] { new int[] { 1, 2 }, new int[] { 3 } });
        var item1Clone = Tuple.Create(1, new int[][] { new int[] { 1, 2 }, new int[] { 3 } });
        var item2 = Tuple.Create(1, new int[][] { new int[] { 1, 3 }, new int[] { 3 } });

        var set = new HashSet<Tuple<int, int[][]>>(StructuralEqualityComparer<Tuple<int, int[][]>>.Default);
        Console.WriteLine(set.Add(item1));      //true
        Console.WriteLine(set.Add(item1Clone)); //false
        Console.WriteLine(set.Add(item2));      //true

我们还可以通过实现这些接口,使我们自己的容器与其他容器良好地配合使用。
public class StructuralLinkedList<T> : LinkedList<T>, IStructuralEquatable
    {
        public bool Equals(object other, IEqualityComparer comparer)
        {
            if (other == null)
                return false;

            StructuralLinkedList<T> otherList = other as StructuralLinkedList<T>;
            if (otherList == null)
                return false;

            using( var thisItem = this.GetEnumerator() )
            using (var otherItem = otherList.GetEnumerator())
            {
                while (true)
                {
                    bool thisDone = !thisItem.MoveNext();
                    bool otherDone = !otherItem.MoveNext();

                    if (thisDone && otherDone)
                        break;

                    if (thisDone || otherDone)
                        return false;

                    if (!comparer.Equals(thisItem.Current, otherItem.Current))
                        return false;
                }
            }

            return true;
        }

        public int GetHashCode(IEqualityComparer comparer)
        {
            var result = 0;
            foreach (var item in this)
                result = result * 31 + comparer.GetHashCode(item);

            return result;
        }

        public void Add(T item)
        {
            this.AddLast(item);
        }
    }

现在我们可以创建一个 HashSet,其中的项目具有包含在自定义容器中的容器。
        var item1 = Tuple.Create(1, new StructuralLinkedList<int[]> { new int[] { 1, 2 }, new int[] { 3 } });
        var item1Clone = Tuple.Create(1, new StructuralLinkedList<int[]> { new int[] { 1, 2 }, new int[] { 3 } });
        var item2 = Tuple.Create(1, new StructuralLinkedList<int[]> { new int[] { 1, 3 }, new int[] { 3 } });

        var set = new HashSet<Tuple<int, StructuralLinkedList<int[]>>>(StructuralEqualityComparer<Tuple<int, StructuralLinkedList<int[]>>>.Default);
        Console.WriteLine(set.Add(item1));      //true
        Console.WriteLine(set.Add(item1Clone)); //false
        Console.WriteLine(set.Add(item2));      //true

6
在“备注”部分中,IStructuralEquatable接口的描述如下:
IStructuralEquatable 接口使您能够实现自定义比较,以检查集合对象的结构相等性。”
这也可以从该接口位于System.Collections命名空间中得到证明。

5

这里是另一个示例,说明了两个接口的可能用法:

var a1 = new[] { 1, 33, 376, 4};
var a2 = new[] { 1, 33, 376, 4 };
var a3 = new[] { 2, 366, 12, 12};

Debug.WriteLine(a1.Equals(a2)); // False
Debug.WriteLine(StructuralComparisons.StructuralEqualityComparer.Equals(a1, a2)); // True

Debug.WriteLine(StructuralComparisons.StructuralComparer.Compare(a1, a2)); // 0
Debug.WriteLine(StructuralComparisons.StructuralComparer.Compare(a1, a3)); // -1

1
顺便提一下,为你的StructuralEqualityComparer添加一个通用类型约束可能是个好主意。例如:where T : IStructuralEquatable - AndrewS

2

C# in a nutshell 书:

由于 Array 是一个类,所以无论数组的元素类型如何,数组始终是(它们自己)引用类型。这意味着语句 arrayB = arrayA 会导致两个变量引用同一数组。同样,除非使用自定义相等比较器,否则两个不同的数组将始终无法通过相等性测试。Framework 4.0 引入了一个用于比较数组中元素的相等比较器,您可以通过 StructuralComparisons 类访问该比较器。

object[] a1 = { "string", 123, true};
object[] a2 = { "string", 123, true};

Console.WriteLine(a1 == a2);               // False
Console.WriteLine(a1.Equals(a2));          // False

IStructuralEquatable se1 = a1;
Console.WriteLine(se1.Equals(a2, StructuralComparisons.StructuralEqualityComparer));    // True
Console.WriteLine(StructuralComparisons.StructuralEqualityComparer.Equals(a1, a2));     // True

object[] a3 = {"string", 123, true};
object[] a4 = {"string", 123, true};
object[] a5 = {"string", 124, true};

IStructuralComparable se2 = a3;
Console.WriteLine(se2.CompareTo(a4, StructuralComparisons.StructuralComparer));    // 0
Console.WriteLine(StructuralComparisons.StructuralComparer.Compare(a3, a4));       // 0
Console.WriteLine(StructuralComparisons.StructuralComparer.Compare(a4, a5));       // -1
Console.WriteLine(StructuralComparisons.StructuralComparer.Compare(a5, a4));       // 1

0

F#自.NET 4开始使用它们。(.net 2在这里)

这些接口对于F#至关重要。

let list1 = [1;5;9] 
let list2 = List.append [1;5] [9]

printfn "are they equal? %b" (list1 = list2)

list1.GetType().GetInterfaces().Dump()

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接