views:

93

answers:

4

I want to write a generic function that has a constraint on the type. Specifically I want something like this:

bool IsInList<T>(T value, params T[] args)
{
    bool found = false;
    foreach(var arg in args)
    {
        if(arg == value)
        {
            found = true;
            break;
        }
    }
    return found;
 }

The point being that you can check if an item is in a parameter list viz:

if(IsInList("Eggs", "Cheese", "Eggs", "Ham"))

However, the compiler croaks on the equality line. So I want to put in a constraint on the type that it implements IEquatable. However, constraints only seem to work at the class level. Is this correct, or is there some way to specify this generically?

A: 

The `==' operator is generally used for immutable types only - (built-in value types or custom immutable types that have overloaded the == operator. Unless you overload it, if you use it on reference types, (except strings) it only returns true if both variables point to the same instance of the type (same object). It would return false if the two veriables point to different instances of the type even if they both have all the same internal data values.

So You need to restrict the type T to those types that implement the Equals() function, which is intended to determine whether two instances of any type are "equal" and not just that they both point to the same instance... and use that instead. The built-in interface IEquatable<T> expresses this for you. (kinda like IComparable<T> requires that a type has a CompareTo() function)
Also, you can make your function much terser and clearer...

try this:

bool IsInList<T>(T value, params T[] args) where T:IEquatable<T>
{ 
    foreach(var arg in args)          
        if(value.Equals(arg)) return true;
    return false;
 } 

You should also understand the difference between Equals() and == and decide which is more appropriate for whatever your intent is... Check out this reference for more info.

Charles Bretana
That doesn't work, `IEquatable<T>` doesn't contain `operator ==` and it's not somehow magically created either. Also, `Equals()` works even without `IEquatable<T>` (although it's `Equals(object)` and not `Equals(T)`).
svick
Yes, but you can't expect to have the compiler implement the code for an Equals method for you on any arbitrary custom type you have defined. You have to write that code yourself. This constraint just restricts you from calling the method on types that have not implemented this method. And, yes, also, you are correct in that the == operator is not in IEquatable my mistake, I edited to correct that... Check out the link I added for more info.
Charles Bretana
@Charles: No, it's not quite the same thing. The signature of `IEquatable<T>.Equals` isn't the same as `object.Equals`. By constraining it this way, you're preventing it from working on types where the default equality comparison works perfectly well. Basically there's unfortunately no way of saying, "constrain T to a type which has a custom equality comparison" - your current constraint is too narrow, and "no constraint" is arguably too wide.
Jon Skeet
A: 

Just replace

arg == value

with

arg.Equals(value)
Matthew Manela
A: 

Generic constraints work on generic methods as well:

bool IsInList<T>(T value, params T[] args) where T : IEquatable<T>

BUT, IEquatable<T> doesn't define operator ==, only Equals(T).

So, you should use Equals() and you don't even need the constraint for that: Equals(object) is member of object.

Also don't forget that Equals won't work if the object is null.

svick
+3  A: 

Others have mentioned IEquatable<T> which is certainly a good potential constraint.

Another option to remember is EqualityComparer<T>.Default, which will use IEquatable<T> if available, but fall back to object.Equals(object) otherwise. This means you can use it with types which predate generics but override Equals, for example:

bool IsInList<T>(T value, params T[] args)
{
    IEqualityComparer<T> comparer = EqualityComparer<T>.Default;
    bool found = false;
    foreach(var arg in args)
    {
        if(comparer.Equals(arg, value))
        {
            found = true;
            break;
        }
    }
    return found;
}

Note that the default equality comparer also copes with null references, so you don't need to worry about those yourself. If type T hasn't overridden object.Equals(object) or implemented IEquatable<T>, you'll get reference equality semantics (i.e. it will only return true if the exact reference is in the array).

A few other points:

  • Your desire to stick to a single exit point makes the code less readable, IMO. There's no need for an extra variable here:

    bool IsInList<T>(T value, params T[] args)
    {
        IEqualityComparer<T> comparer = EqualityComparer<T>.Default;
        foreach (var arg in args)
        {
            if (comparer.Equals(arg, value))
            {
                return true;
            }
        }
        return false;
    }
    
  • LINQ already contains a method for this, Contains, so you can simplify the code to:

    bool IsInList<T>(T value, params T[] args)
    {
        return args.Contains(value);
    }
    
  • Array effectively contains this functionality too, with IndexOf:

    bool IsInList<T>(T value, params T[] args)
    {
        return Array.IndexOf(args, value) != -1;
    }
    
  • Your method is perhaps a little misleadingly named, given that args is an array, not a List<T>.

Jon Skeet
+1 for the very detailed answer, but I think you have a typo in your first code: if(comparer.Equals(arg == value)) - I think it should be if(comparer.Equals(arg, value)).
ShdNx
@ShdNx, I agree, hope Jon doesn't mind the edit. ;)
Kirk Woll
@ShdNx: Whoops, absolutely :)
Jon Skeet
@Jon Skeet: Isn't `IndexOf` just a static method on `System.Array`?
Ani
@Ani: Doh again... yes :)
Jon Skeet