10

I'm trying to define a method on a generic class that is limited to a specific type. I have come up with this:

interface IHasId 
{
    int Id { get; }
}

public class Foo<T>
{
    private List<T> children;

    public IHasId GetById(int id)
    {
        foreach (var child in children.Cast<IHasId>())
        {
            if (child.Id == id)
            {
                return child;
            }
        }

        return null;
    }
}

It will work, but it looks like a code smell...it seems like there should be a way to get the compiler to enforce this. Something like:

public class Foo<T>
{
    public IHasId GetById<TWithId>(int id) where TWithId : IHasId {}
}

or, even better:

public class Foo<T>
{
    public IHasId GetById(int id) where T : IHasId {}
}

I saw a few posts on this related to Java, and one talking specifically about constraining T to an enum, but nothing directly on point.

John Riehl
  • 1,270
  • 1
  • 11
  • 22

5 Answers5

5

You can't have optional methods based on a single type. You can, however, use inheritance to make it work.

Here's how:

public interface IHasId 
{
    int Id { get; }
}

public class Foo<T>
{
    protected List<T> children;
}

public class FooHasId<T> : Foo<T> where T : IHasId
{
    public IHasId GetById(int id)
    {
        foreach (var child in children)
        {
            if (child.Id == id)
            {
                return child;
            }
        }
        return null;
    }
}

With C#6 FooHasId could be shortened to this:

public class FooHasId<T> : Foo<T> where T : IHasId
{
    public IHasId GetById(int id) => this.children.FirstOrDefault(x => x.Id == id);
}
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
3

You can add more than one constraint to a generic, for instance here, the generic in the GetById method must be T and IHasId:

using System.Collections.Generic;
using System.Linq;

public interface IHasId
{
    public int Id { get; }
}

public class Foo<T>
{
    private List<T> children;

    public TItem GetById<TItem>(int id)
        where TItem : T, IHasId
    {
        return children.Where(t => t is TItem).Cast<TItem>().FirstOrDefault(t => t.Id == id);
    }
}

Fiddle: https://dotnetfiddle.net/bD2Mpl

hardkoded
  • 18,915
  • 3
  • 52
  • 64
  • Why do `children.Cast()` on a collection of type `T`? – Enigmativity May 28 '17 at 23:58
  • Thanks @Enigmativity I just copied, the code posted, how about now? :) – hardkoded May 29 '17 at 00:01
  • What does `Find` do when it doesn't find anything? – Enigmativity May 29 '17 at 00:05
  • It will return `default(T)` – hardkoded May 29 '17 at 00:09
  • @Enigmativity .NET Source code https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/collections/generic/list.cs#L431 – hardkoded May 29 '17 at 00:10
  • If you're changing the OP's code it would be worth explaining that in the answer. – Enigmativity May 29 '17 at 00:11
  • `return null;` won't compile (Cannot convert null to type T), you answer is also returning default(T), And Find is faster than FirstOrDefault, see: https://stackoverflow.com/questions/14032709/performance-of-find-vs-firstordefault – hardkoded May 29 '17 at 00:13
  • `.Find(...)` isn't very explicit as to its full behaviour (hence why I asked), but `.FirstOrDefault(...)` is clear. – Enigmativity May 29 '17 at 00:16
  • "clear" is relative. Find is faster than FirstOrDefault because that's implemented in the List class itself. If the name Find is not clear, ask Microsoft to change it :p – hardkoded May 29 '17 at 00:19
  • Sorry, perhaps I should have said "clearer". Just trying to help you get a good quality answer. That's what we do here on SO. – Enigmativity May 29 '17 at 00:22
  • @Enigmativity Yeah man, I appreciate it, I added a few comments, just some healthy debate. – hardkoded May 29 '17 at 00:22
  • @kblok Thanks, but this won't help me since it constrains the entire class and I don't want that...just one method in the class that has the constraint. – John Riehl May 29 '17 at 00:37
  • @kblok I may still be missing something, but it looks like `public class Foo where T : IHasId` still enforces the constraint on the whole class, not just on GetById() – John Riehl May 29 '17 at 12:40
  • That's true @JohnRiehl we don't need that constraint, I removed it. – hardkoded May 29 '17 at 12:44
  • This is very good and would definitely work...the only reason I accepted the answer from @Enigmativity rather than this one is that it seems more strictly correct to not have to use `Cast<>()`. – John Riehl May 29 '17 at 13:10
  • @JohnRiehl notice that solution is not using a generic in the method, so you won't be able to do `GetById(22).MyCoolMethod();` you will have to cast that after calling the method, so you'll (maybe) end up casting the object. – hardkoded May 29 '17 at 13:12
  • @JohnRiehl - This answer doesn't work because you're forced to explicitly provide a type for the method call and if there exists one item in the collection that is not of that type this will fail. You lose strong-typing and rely on callers to know the types within the collection. – Enigmativity May 29 '17 at 13:27
  • @Enigmativity you're defining the Generics perfectly in that statement. – hardkoded May 29 '17 at 13:30
  • What is the point of having a generic method if the method is not generic? You should not need to cast. Plus `where TItem : T, IHasId` makes no sense. This is not how generics work. – CodingYoshi May 29 '17 at 13:33
  • @kblok `GetById()`'s return type is `TItem`, so I wouldn't think I'd have to cast it. – John Riehl May 29 '17 at 13:54
  • @Enigmativity True, this requires explicit typing, but the `Select()` statement filters `children` to just those of type `TItem`, so I don't think it will fail in a mixed collection. Your solution is more correct, but this would work too. – John Riehl May 29 '17 at 13:57
  • @kblok - How am I defining generics in that statement? – Enigmativity May 29 '17 at 14:40
  • @JohnRiehl - If you run the code you immediately get an `InvalidCastException` "Unable to cast object of type 'System.Boolean' to type 'T1'.". – Enigmativity May 29 '17 at 14:46
  • @JohnRiehl - You can change the `.Select` to a `.Where` and the code almost works. The problem comes if you have multiple types in the `children` collection that implement `IHasId`. You'd have to perform a search explicitly on all of the types that you **think** are in the collection to find one that works. You can't hard-code this as any new implementation can be created that this code can't be aware of. – Enigmativity May 29 '17 at 14:50
  • Good catch @Enigmativity, example changed, fiddle uploaded. – hardkoded May 29 '17 at 14:56
  • 1
    Thanks for the method syntax reference. – Nae Sep 06 '19 at 12:58
2

You can do this with Method Extension, however, you are not able to mix the Types in the list, as the Method extension have to be explicit.

  • Method1(this Foo intFoo, int id)
  • Method2(this Foo stringFoo, string name)

Extended Methods does not recognize interfaces if you have used a Type as your Generic Type, so you need to be very specific.

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            Foo<IHasName> nameList = new Foo<IHasName>(new List<IHasName>()
            {
                new ObjectWithName("banana"),
                new ObjectWithName("apple")
            });

            Foo<IHasId> idList = new Foo<IHasId>(new List<IHasId>()
            {
                new ObjectWithId(1),
                new ObjectWithId(2),
                new ObjectWithId(3)
            });

            var obj1 = nameList.GetByName("banana");
            var obj2 = idList.GetById(2);
        }
    }

    public class ObjectWithName : IHasName
    {
        public string Name { get; }
        public ObjectWithName(string name)
        {
            Name = name;
        }
    }

    public class ObjectWithId : IHasId
    {
        public int Id { get; }
        public ObjectWithId(int id)
        {
            Id = id;
        }
    }

    public interface IHasName
    {
        string Name { get; }
    }

    public interface IHasId
    {
        int Id { get; }
    }

    public class Foo<T> : IEnumerable<T>
    {
        private IList<T> children;

        public Foo(IList<T> collection)
        {
            children = collection;
        }

        public IEnumerator<T> GetEnumerator()
        {
            return children.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return children.GetEnumerator();
        }
    }

    public static class FooExtensions
    {
        public static IHasId GetById(this Foo<IHasId> foo, int id)
        {
            return foo.FirstOrDefault(c => c.Id == id);
        }

        public static IHasName GetByName(this Foo<IHasName> foo, string name)
        {
            return foo.FirstOrDefault(c => c.Name == name);
        }
    }
}
Droa
  • 427
  • 7
  • 15
-1

Maybe you can use 2 generic so then you can apply constraints at different levels ? I'm not sure how it would work at compile or runtime if you try to call GetById on a type that violates the constraint though.

public interface IHasId
{
    int Id { get; }
}

public class Foo<T>
{
    private List<T> children;

    public IHasId GetById<SubT>(int id)  where SubT : IHasId
    {
        foreach (var child in children.Cast<IHasId>())
        {
            if (child.Id == id)
            {
                return child;
            }
        }

        return null;
    }
}
Etienne
  • 1,058
  • 11
  • 22
  • This is closer to what I'm looking for. It's not as elegant as the inheritance solution @Enigmativity posted, but it does provide a sort of compiler-enforced type safety without having to subclass. Usage becomes `var result = fooInstance.GetById(id);` If `SomeType` doesn't implement `IHasId` the compiler throws an error. Where it falls short is enforcing that `SomeType` is a subclass of `T` – John Riehl May 29 '17 at 12:52
-3

You can apply a constraint and do it like this (With a little linq, your method will look like this):

public interface IHasId
{
    int Id { get; }
}

public class Foo<T> where T : IHasId
{
    private List<T> children;

    public IHasId GetById(int id)
    {
        return this.children.FirstOrDefault(x => x.Id == id);
    }
}

UPDATE

I am leaving my answer above as is because that clearly answers the OPs question and whomever is downvoting does not understand generics.

@Enigmativity commented in his answer to one of my comments:

No, you're not answering the question. The OP wants the method constrained - and not the entire class. He wants T to be any type.

If the OP wants the method constrained only and not the entire class, then the answer is as below:

Constraint Method Only

You cannot apply a constraint to the method only because when your class is constructed, the generic has to be closed. In other words, you are storing type T(s) in the children field, type T will have to be closed at the class level. Thus, type T has to be constrained at the class level.

If you want the method to be generic only then you cannot use the type T at the class level. Your class does not have to be generic to have a generic method. You can still have a generic method in your class as I have shown below:

public class Foo
{
    // The children list is not of type T, but it has been closed with 
    // type SomeClass
    private List<SomeClass> children;

    // Here we are saying this method will work so long as it is called with
    // any T that implements IHasId
    // The class is not generic, but the method is
    public bool IsIdOver100<T>(T hasId) where T : IHasId
    {
        return hasId.Id > 100;
    } 
}

public class SomeClass : IHasId
{
    public int Id { get { /*...code*/ } }
} 

FYI, @engimativity's answer is also applying the constraint at the class level but indicating to use inheritance. That will still apply the constraint at the class level and the inheritance makes no difference.

@kblok also made a similar comment in the comments to this answer:

he's asking how to apply a constraint in a method, you're not answering the question

And this is the solution, kblok is proposing:

public TItem GetById<TItem>(int id)
        where TItem : T, IHasId
{
    return children.Select(t => t is TItem).Cast<TItem>().FirstOrDefault(t => t.Id == id);
}

There are two issues with the above: 1. The method is casting so it is not generic. 2. The constraint is saying TItem : T which makes no sense since T is not a closed type and there are no constraints placed on what T can be. I am not even sure why the compiler would even allow that but there may be a good reason for it. If T had a constraint at the class level such as where T : IAnotherInterface, then it will make sense to place a constraint at the method level which says: where TItem : T, IHasId but that still leaves the method non-generic if there is casting going on.

Some More Info for OP

If you want to inherit the Foo class, you can still do that with or without closing it. Here is example with closing it:

public class FooOfSomeClass : Foo<SomeClass>
{
    // you can have addition methods, properties stuff here like any other inheriting class
}
CodingYoshi
  • 25,467
  • 4
  • 62
  • 64