Part 4 left us with a reusable, abstract and inference friendly Tuple class. The next step is to be able to test for Tuple equality.
For the Tuple implementation, two tuples will be defined as equal if all of their members are equal. Seems fairly straight forward. The trick is in the implementation. In addition to doing the typical override of Equals/GetHashCode the Tuple implementation will be implementing [IEquatable
For instance what if we are dealing with value types’ Is Equals() the best method to call’ What if the type in question implements [IEquatable
Luckily there is an easy and straight forward solution. The BCL defines a class, [EqualityComparer
There is one small trick to implementing Equals correctly. The implementation explicitly uses Object.ReferenceEquals to check for null rather than ==. The reason being is once operator== is defined for the type Tuple, comparison for even null will bind to this operator. Part of checking for operator== will end up calling Equals and hence you can end in a stack overflow fairly quick. Note that our implementation of == will work around this but it’s still safer to be explicit.
function script:Gen-Equals
{
param ( [int] $count = $(throw "Need a count") )
$OFS = ','
$gen = "<" \+ [string](0..($count-1) | %{ "T"+$upperList[$_] }) + ">"
"public override bool Equals(object obj) { "
"return Equals(obj as Tuple$gen); }"
"public bool Equals(Tuple$gen other) {"
"if ( Object.ReferenceEquals(other,null) ) { return false; }"
"if ("
$OFS = "&&"
[string](0..($count-1) |
%{"EqualityComparer<T{0}>.Default.Equals(m_{1},other.m_{1})" -f
$upperList[$_],$lowerList[$_] })
") { return true; }"
"return false;"
"}"
}
GetHashCode can also utilize [EqualityComparer
function script:Gen-GetHashCode
{
param ( [int] $count = $(throw "Need a count") )
"public override int GetHashCode() {"
"int code = 0;"
0..($count-1) | %{ "code +=
EqualityComparer<T{0}>.Default.GetHashCode(m_{1});" -f
$upperList[$_],$lowerList[$_] }
"return code;"
"}"
}
Both of the operators are likewise straight forward. As before mentioned [EqualityComparer
function script:Gen-OpEquals
{
param ( [int] $count = $(throw "Need a count") )
$OFS = ','
$gen = "<" \+ [string](0..($count-1) | %{ "T"+$upperList[$_] }) + ">"
"public static bool operator==(Tuple$gen left, Tuple$gen right) {"
"return EqualityComparer<Tuple$gen>.Default.Equals(left,right); }"
"public static bool operator!=(Tuple$gen left, Tuple$gen right) {"
"return !EqualityComparer<Tuple$gen>.Default.Equals(left,right); }"
}
In addition to the methods, the Tuple class generation must be changed to implement IEquatable<Tuple<».
Some will notice that the implementation forces the equality comparison to be against a Tuple
- I have come up against specific scenarios where I wanted to compare Tuple
but not ITuple . This is not saying they don't exist (they do). But I prefer to leave an implementation until I find a justification for implementing it. - By constraining to IEquatable<Tuple
> we are always comparing apples to apples. If you try and perform an Equals against ITuple you're leaving yourself open to comparing apples and oranges. Since ITuple<TA,TB> implements ITuple it is a valid target for the overload. This type of equality seems scenario dependent and as such I left it out for the time. Note with our current implementation it would be very easy to come back and add this later. - To make #2 even stranger, once MutableTuples are implemented an implemantation of IEquatable<ITuple
> might actually be comparing Tuple<TA,TB,TC> to MutableTuple .