0

In newer versions of .net, there are a number of extensions methods which accept IEnumerable<T> or IEnumerable. One such method is OfType<TResult> which returns an enumerable that contains only elements of the original sequence which can be cast to the specified type T. This method perfectly happily chug away using a non-generic IEnumerable to process all the items of the original list, regardless of the type of the original list and the destination type. If the original list is an IEnumerable<int> and TResult is int, it will get each item of the original list as a non-generic object and then cast it, even though it could just use the original IEnumerator<int>. If the original list is an IEnumerable<int> and TResult is StringBuilder, it will likewise chug through boxing all the items in the original list even though none of them could possibly be cast to the destination type.

How difficult would it be to write a method which would take an IEnumerable<TSrc> and convert it to an IEnumerable<TResult>, handling efficiently the cases in which either all items in the original enumeration are guaranteed to appear in the latter, or in which the type of the source enumerator would imply that the latter would be empty (assuming that all variations of IEnumerable a type supports would return the same sequence of items)? To be sure, one wouldn't terribly often be trying to cast a sequence to the type it already holds, but it's not uncommon to have a generic class with multiple type parameters that will sometimes overlap and sometimes not.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • Value types cannot derive from a base class. Their only common base will always only be Object, which means you *always* have to box at least one value if you want to compare two different value types without specifically using a method that knows the two types you want to compare--which `Enumerable.Intersect` and `Except` don't accept anyway. So it's not possible without writing different methods. – Peter Ritchie Sep 23 '12 at 18:59
  • @PeterRitchie: I'm assuming that two items of different types may be safely assumed to be unequal. If one of the type parameters is a value type and is not convertible to the other, the items cannot match. If one is a value type and the other isn't, but the former is convertible to the latter and implements `IEquatable`, then one can perform a comparison without any boxing by checking to see if the type of the second matches the first and, if so, casting the second (already a reference type, so no boxing) to `IEquatable` and using `IEquatable.Equals(firstObject)`. – supercat Sep 23 '12 at 19:21
  • that would be an unsafe assumption because disparate types can be compared--which is part of why Object.Equals is overridable. For example you the following results in true: byte b = a;int i = 1;var t = b.Equals(i) – Peter Ritchie Sep 23 '12 at 19:29
  • the point being that equality is encapsulated in Equals and while two types that can be converted between each other can the be compared, you don't know if Equals is doing that – Peter Ritchie Sep 23 '12 at 19:35
  • @PeterRitchie: The reason that `b.Equals(i)` is not that `b` *overrides* `Object.Equals`, but rather that it *overloads* equals with comparisons for numeric types. Cast either operand to `Object` and the comparison will return false; I am unaware of any type in the Framework which will overload `Object.Equals` in such a fashion as to return `true` when comparing itself to any object of any other types, other than value types and nullable types which will compare as equal to their boxed equivalents. – supercat Sep 23 '12 at 21:31
  • @PeterRitchie: Although it would be possible for a pair of types `A` and `B` to override `Object.Equals(Object)` such that an instance of type `A` would report itself equal to an instance of type `B`, it's generally hard to ensure that such relationships are always reflective. Further, such a thing would not be consistent with the expected behavior of `Object.Equals` which generally implies a strong degree of equivalence. Although the `Single`, `Double`, and `Decimal` override `Equals` to mean something other than equivalence, for most .net types, if `Thing1.Equals(Thing2)`, one may... – supercat Sep 23 '12 at 21:40
  • ...replace all references to `Thing1` with references to `Thing2` without affecting anything other than `ReferenceEquals`. (if `Thing1` and `Thing2` are class types with the default `Equals` method, the comparison would only return true if `Thing1` and `Thing2` were the same object, making the substitution a no-op; if they override `Equals`, they should generally only do so when such equivalence would hold. – supercat Sep 23 '12 at 21:43
  • that's just how some of the Equals methods are written. A boxed type cannot be converted to a different boxed type (without unboxing) I show an example in my answer of an implementation of Equals that would return true for a boxed type (updated the example). In fact those implementation of Equals are in violation of Framework guidelines because many return different values than operator== – Peter Ritchie Sep 23 '12 at 21:44
  • Based on that it's hard to guarantee these relationships are reflexive and that there's nothing you do with existing `Enumerable.Intersect` and `Enumerable.Except` to work with disparate types (other than to cast to Object), clearly you can't do what you're asking. Over and above the fact that types *could* exist that violate *non-convertible equals non-equal*. – Peter Ritchie Sep 23 '12 at 21:48
  • @PeterRitchie: It is generally accepted that if types override `Equals` and `GetHashCode` in ways that are inconsistent with normal conventions, operations on collections of such types should not be expected to work sensibly. For example, if calling `GetHashCode` on some object `Foo` returns 23, and a `Dictionary` does not contain any object whose `GetHashCode` method returned 23 when it was added, the `Contains` method is allowed to return false, even if the `Dictionary` contains some object which would report itself equal to `Foo`. Such behavior would not be considered a bug in `Dictionary`. – supercat Sep 23 '12 at 21:58
  • I don't see how that's relevant. e.g. I've updated the `One`/`Two` example so they implement `GetHashCode`, so those two type are consistent with normal conventions. If I updated One to be reflexive in equality with Two there's still be the issue that the two types are not convertible between each other but can be equal. The fact remains that it's up to the type to define what equality is, it's free to (and recommended to) compare value, which is what `Two` does, just because it knows about `One` is irrelevant. – Peter Ritchie Sep 23 '12 at 22:20
  • @PeterRitchie: Can you identify *any* Framework types that work in such a fashion? I would consider it entirely reasonable for an `IEqualityComparer` to define an equivalence relation which would regard as equal things of different types (derived from `T`) since custom equality comparers are often expected to regard non-identical things as equivalent (e.g. some string comparers would regard "Hello" and "HELLO" as equivalent) but I would expect types' default equality-comparison method to be stricter than that. – supercat Sep 23 '12 at 22:33
  • I'm not saying that there's a pattern there for implementing `Equals. (Object)`. You clearly can't test for equality on types you don't know about or don't want to couple to. `MailAddress` is an example `Object.Equals(new MailAddress("e@t.com"), "e@t.com")` results in true. There's nothing stopping anyone from implementing a class that implements Equals this way--which makes what you want do (outside of Intersect/Except) questionable. w.r.t. Intersect/Except, they simply don't support what you want, the way you've described it. – Peter Ritchie Sep 24 '12 at 00:47
  • What about `Object.Equals("e@t.com", new MailAddress("e@t.com"))`? If that would return `false` (as I'm 99.44% certain it will) but the former results in true, that would suggest `MailAddress` has a malformed `Object.Equals` implementation. The existing implementations of intersect/except are limited to sequences of the same type as the original, though conceptually there's no reason they would have to be. – supercat Sep 24 '12 at 01:05
  • `Object.Equals("t@t.com", mailAddress)` returns `false`. `ContentType`, and `ContentDisposition` do the same, `UriBuilder` will equal `Uri` objects by value (again, not symmetric), etc. Whether or not you define that as malformed is irrelevant 1) equality is "malformed" with many types in different ways other than this and 2) there's not standard for "malformed" w.r.t to `Object.Equals(Object x, Object y)`. – Peter Ritchie Sep 24 '12 at 01:45
  • @PeterRitchie: There is a standard for "malformed" `Equals`, in that http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx says "The following statements must be true for all implementations of the Equals(Object) method (list omitted)" If a standard says all X "must" have some characteristic, then any X lacking such a characteristic is, *by definition*, malformed. – supercat Sep 24 '12 at 14:37
  • And that's the problem because that also details "must also override the Equals(Object) method to return the same result as the equality operator"--which means all intrinsic types are "malformed" *by definition* – Peter Ritchie Sep 24 '12 at 14:51
  • @PeterRitchie: When the C# language sees the `==` operator with certain combinations of operand types, it sometimes does things other than invoke those types' overloads of the equality-test operator; such a behavior is a function of the C# language rather than the equality operators. That having been said, I must say I don't happen like that last rule you cited, since there are cases where the most logical behavior for an equality operator differs from the logical behavior for `Equals(Object)`. For examine, if one had a "CaseInsensitiveString" type, it would be logical... – supercat Sep 24 '12 at 15:29
  • ...for `(CaseInensitiveString)"HELLO" == (CaseInsensitiveString)"Hello"` to return true (neither string could be greater than the other while allowing case-insensitive sorting), but `((Object)(CaseInseesitiveString)"HELLO").Equals((Object)(CaseInseesitiveString)"Hello")` should return `false` (since the things in question aren't "equal"). I really dislike the way C# handles the `==` operator, btw, since it serves to muddy the waters still further. – supercat Sep 24 '12 at 15:34
  • @PeterRitchie: Incidentally, the `==` operator does not define an equivalence relation. `123456789123456789==123456789123456789.0` is true, as is `123456789123456788==123456789123456789.0`, but `123456789123456788==123456789123456789` is false. I would regard making `==` and `Equals(Object)` behave identically as being far less important than having all overrides of `Equals(Object)` define a proper equivalence relation. The latter is achievable only if one forgoes the former. – supercat Sep 25 '12 at 15:13

1 Answers1

0

IEquatable<T>.Equals should be consistent with the Object.Equals override. I'm not sure specifically using IEquatable<T> directly offers any value.

It sounds to me like you simply want to make sure boxed objects use the unboxed values for the equality test. You should be able to do most of what you want with an equals like this:

private static bool Equals<T1, T2>(T1 first, T2 second)
{
    // if either are boxed, try Equals with unboxed values (dynamic)
    if (typeof(T2) == typeof(object) && second.GetType().IsValueType)
    {
        dynamic dsec = second;
        if (typeof(T1) == typeof(object) && second.GetType().IsValueType)
        {
            dynamic dfir = first;
            return Equals(dfir, dsec);
        }
        return Equals(first, dsec);
    }
    if (typeof(T1) == typeof(object) && second.GetType().IsValueType)
    {
        dynamic dfir = first;
        Equals<dynamic, T2>(dfir, second);
    }
    // neither are boxed, just fall back to their Equals overrides...
    var t = Object.Equals(first, second);
    return t;
}

Enumerable.Intersect and Except work only with collections of the same type, so if the type was Object it should do what you want.

Update:

Value types cannot derive from a base class. Their only common base will always only be Object, which means you always have to box at least one value if you want to compare two different value types without specifically using a method that knows the two types you want to compare--which Enumerable.Intersect and Except don't accept anyway. So it's not possible without writing different methods.

Update 2:

It's a poor assumption to assume that if two types can't been converted between each other (or one to the other) that that means they can never be equal. For example:

public struct One
{
    public int Value;
    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
}

public struct Two
{
    public int Value;
    public override bool Equals(object obj)
    {
        if (obj is Two) return this == (Two)obj;
        if (obj is One) return this.Equals((One)obj);
        return base.Equals(obj);
    }

    public bool Equals(One one)
    {
        return one.Value == Value;
    }

    public static bool operator==(Two one, Two two)
    {
        return one.Value == two.Value;
    }

    public static bool operator !=(Two one, Two two)
    {
        return !(one == two);
    }
    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
}

// ...

    Console.WriteLine(two.Equals(one));
    Console.WriteLine(two.Equals((Object)one));
    Console.WriteLine(two.GetType().IsAssignableFrom(one.GetType()) || one.GetType().IsAssignableFrom(two.GetType()));

Results in True, True and False. So, clearly two objects can be "equal" while not being convertible between each other.

Community
  • 1
  • 1
Peter Ritchie
  • 35,463
  • 9
  • 80
  • 98
  • As written, your code violates the rule that types should override `Equals(Object)` in such fashion as to define an equivalence relation, which would in turn imply that `Object.Equals(X,Y)` should equal `Object.Equals(Y,X)` for any `X` and `Y`. Even if one added code to struct `One` to override its `Equals` method to make it reflexive with that of `Two`, such behavior would still be very different from anything I know of in the Framework. Can you identify any situation where `Object.Equals(X,Y)` can be true even though `X.GetType() != Y.GetType()`, and `X` and `Y` are both Framework types? – supercat Sep 23 '12 at 21:50
  • Seems irrelevant to your original question since many framework types violate guidelines too. Like: `Byte b = 10; Int32 i = b; Debug.Assert(b.Equals(i) == (b == i));` Clearly Byte is not correctly defining an equivalence relation as defined by "Override the Equals method whenever you implement the equality operator (==), and make them do the same thing." at http://bit.ly/SnmMQm – Peter Ritchie Sep 23 '12 at 22:10
  • Many overloads of the `==` operator will cause both sides to be converted the same type *before doing the comparison*. In your example, both operands to the `==` operator will be converted to type `Int32`. Some types also define *overloads* of `Equals`, which may also cause conversion before comparison. Note, however, that methods to manipulate generic collections types are essentially never expected to use overloads of `==` or `Equals`, but rather use a supplied equality comparer, or else use either `IEquatable.Equals(T)` or the virtual `Equals(Object)`. – supercat Sep 23 '12 at 22:29
  • Yes, and that's irrelevant with regard to the guideline as it refers to Equals(object) because Equals(object) always takes an object parameter and *cannot* convert and must box – Peter Ritchie Sep 23 '12 at 23:09