0

I'm trying to build a list of simple recipes, which will essentially track a list of "ingredients" and the product they produce.

My issue is that with my Recipe class, I'd prefer to pass the class which represents the final product rather than an instance of it. I'm not as familiar with generics and passing type in C#, so I'm looking for advice.

If I have:

public class Bread : Food {
  //...
}

I want to pass the class, not an instance.

var bread = new Recipe(Bread, ... ingredients);

The only caveat is that it can't be too generic, every argument must be a Food type.

Only once the recipe is asked to "produce" an instance of the product will I call Activator.CreateInstance.

I mainly wish to avoid sending an instance because I'd have to clone it rather than instantiate it.

helion3
  • 34,737
  • 15
  • 57
  • 100
  • `I want to pass the class, not an instance` that doesn't really make sense. You have to send *something* - an instance, a `Type` object, a string, something. I guess you want to send a lot of `Type`s – Jonesopolis Jan 12 '17 at 21:34
  • Maybe my terminology doesn't mesh with C# correctly but in java it would be `public Recipe(Class product, ...)` – helion3 Jan 12 '17 at 21:41

2 Answers2

1

You can use generics when defining your Recipe class, and constrain the generic type to the Food class, or classes derived from Food.

From the MSDN documentation:

When you define a generic class, you can apply restrictions to the kinds of types that client code can use for type arguments when it instantiates your class. If client code tries to instantiate your class by using a type that is not allowed by a constraint, the result is a compile-time error. These restrictions are called constraints. Constraints are specified by using the where contextual keyword. The following table lists the six types of constraints:

Further down in that article is a table that specifies how to achieve the result you want. In your case, your Recipe declaration will look like this:

public class Recipe<T> where T : Food, which can define constructors as usual

new Recipe(params string[] ingredients) notice there's no call to Bread.

When you instantiate Recipe, you can use var recipe = new Recipe<Bread>(ingredientsList).

If you try and declare the Recipe with something that doesn't derive from Food, you'll get an exception during design-time, which will inform you that

The type cannot be used as type parameter 'T' in generic type of method 'Recipe'. There is no boxing conversion from to 'Food'.

If you need help with generics, this MSDN article might be a good starting point.

UPDATE: As per the comments, another way to implement this is to push the generic to a Create method.

Your code would look something like this:

public class Food { }
public class Bread :Food { }
public class Recipe
{
    public T Create<T>() where T : Food, new()
    {
        return new T();
    }
}

I've also added the new constraint (you need that in this case since passing a static class for instance would cause a runtime exception). You can now store a Recipe in a list, but now your logic will have to provide the specific type you want to invoke when the object is being created, rather when the recipe is being created.

If you want to determine the type at runtime (so basically store the type in the recipe for later creation add a constructor like thispublic Recipe(T typeToCreate) and follow the methodology in this SO answer. This allows you to store the type.

Community
  • 1
  • 1
Kolichikov
  • 2,944
  • 31
  • 46
  • This works if the resulting object should have a type depending on the type passed in during creation. That can be a hassle if you want to put a lot of recipes into a collection, like a recipebook. If you want a single non-generic `Recipe` type, then instead of `new Recipe(...)` you'd use `Recipe.Create(...)` and define it as `class Recipe { pubic static Recipe Create(...) where T : Food {...} }` – Ben Voigt Jan 12 '17 at 21:45
  • Also consider `where T : new()` – Ben Voigt Jan 12 '17 at 21:45
  • @BenVoigt I do have a list, but I'm not sure if it'll be an issue. Since everything is a subclass of `Food` wouldn't it be acceptable to do `List>`? – helion3 Jan 12 '17 at 21:59
  • I'm guessing it doesn't work well as a parameter to other methods either, it errors that `Recipe` can't be passed to a method which accepts `Recipe` – helion3 Jan 12 '17 at 22:01
  • @BenVoigt You're right. Create will allow you to pass the Recipe object around and store it as a list of Recipies, and only worry about the type when you are creating. For some reason I thought OP would be storing a list of foods, but with the deferred creation your idea makes more sense. And yes, you would need to either make other classes generic (so you could use Recipe in your methods), or do what Ben has suggested and move the generic parameter to a create method. – Kolichikov Jan 12 '17 at 22:10
  • How would I store `T` with `Recipe Create` though? Since it's a static method I'd have to pass that to a new instance of Recipe, which is sort of the original issue. – helion3 Jan 12 '17 at 22:20
  • I'm not sure I see how the Create method ties T to the recipe. In your latest example it just lets me assign it. No matter what recipe I have I can change the output. – helion3 Jan 12 '17 at 22:39
  • @helion3: Inside the `Create` method you can use `T`, and you can also write a lambda like `() => new T()` and save that in a `Func` member field to use when "the recipe is used to *produce* the product" as stated in the question. – Ben Voigt Jan 12 '17 at 23:05
  • @helion3: Another way is to make `Recipe` a generic class, but have it implement a non-generic interface. Then you can have `List` and store inside `Recipe`, `Recipe`, etc. – Ben Voigt Jan 12 '17 at 23:08
0

You can either:

public class Recipe<T> where T:Food
{
    public Recipe(params object[] ingredients)
    {

    }
}

Or:

public class Recipe
{
    public Recipe(Type foodType, params object[] ingredients)
    {
        if (!foodType.IsSubclassOf(typeof(Food)))
        {
            throw new ArgumentException($"{nameof(foodType)} must be a subclass of {nameof(Food)}.");
        }
    }
}

EDIT:

And as @BenVogit mentioned in his comment, if you do not want Recipe to carry a type parameter but do want type safety, you can also make the factory method generic.

Lifu Huang
  • 11,930
  • 14
  • 55
  • 77
  • 1
    A better alternative to the second case is a generic factory function (the `Recipe` class still isn't generic, but the factory function is allowing compile-time type checking) – Ben Voigt Jan 12 '17 at 21:46