jeudi 15 octobre 2020

Cast Type to Generic

I don't know the best way to phrase this question in the title, so I apologise if it's not the best.
However I believe the explanation is easy to follow.

I am making a small C# command line shell, and I am implementing each command (e.g. ls, cat) as a separate class. I am using the CommandLineParser library to parse command line options.

Here is my code:

//Command.cs
public abstract class Command<T>
{
    public void Run(string[] args)
    {
        Parser.Default.ParseArguments<T>(args)
            .WithParsed(ParseOpts)
            .WithNotParsed(HandleParseError);
    }

    public abstract void ParseOpts(T opts);
    public abstract void HandleParseError(IEnumerable<Error> errs);
}

//Cat.cs
public class Cat : Command<CatOptions>
{
    //...
    public override void ParseOpts(CatOptions opts)
    {
        //...
    }

    public override void HandleParseError(IEnumerable<Error> errs)
    {
        //...
    }
    //other stuff...
}

//CatOptions.cs
class CatOptions
{
    [Option('E', "show-ends", Default = False, 
        HelpText = "display $ at end of each line")]
    public bool ShowEnds { get; set; }

    //other options...
}

The way the parser works is that I have to call the boilerplate code that is in Run (i.e. ParseArguments), which will parse the options in args based on the options specified in the options class T. If parsing is successful, it will call ParseOpts, where the command can access the options and do stuff, or HandleParseError if it fails.

I want it so that I don't have to repeat this boilerplate code, but rather just specify the relevant options type T, and implement ParseOpts and HandleParseError as I see fit.

So far, this all makes sense, but I don't know to use it as I want to.

public class Shell
{
    //What do I put here?
    private Dictionary<String, ???> Commands = new Dictionary<String, ???>
    {
        {"cat", Cat}, // or new Cat()? typeof(Cat)?
        //other commands...
    };

    //other stuff...

    private void Execute()
    {
        string input = Console.ReadLine();
        string[] args = ParseInput(input);

        string cmd = args[0];

        if (Commands.ContainsKey(cmd))
        {
            //What I want to be able to do
            Commands[cmd].Run(args);
        }
        else
        {
            //...
        }
    }
    //...
}

In my main Shell class, I have a Dictionary Commands which I use to map command names to their classes. What I want to be able to do is simply get input from the user, parse the command from the input, check if it's in the dictionary, and then Run that command, but that's where I am stuck.

I can't declare Commands as something like Dictionary<String, Command<T>>, because I have to specify what T is. I don't know if it's possible to somehow declare Commands with a generic generic type or something like that.

I also tried storing the Commands as Type, and then instantiating them at runtime and calling Run; like this:

private Dictionary<String, Type> Commands = new Dictionary<String, Type>
    {
        {"cat", typeof(Cat)},
        //other commands...
    };

But I don't know how to instantiate the class and call Run in Execute because I need to somehow cast it to a Command<T> and specify the appropriate T at runtime:

if (Commands.ContainsKey(cmd))
{
    //Want to do something like this, but obviously it doesn't work
    //because I have to provide the T type
    var cmdType = Commands[cmd];
    ((Command<T>)Activator.CreateInstance(cmdType)).Run(args);
}

At runtime I know that regardless of what class it is they are all of type Command<T>, and I can determine the actual class and type T based on the command name (e.g. cat maps to CatOptions).

I know I can do explicit casting like this with if statements:

if (cmdType == typeof(Cat))
{
  ((Cat)Activator.CreateInstance(cmdType)).Run(args);
}
else if ...

But I don't want to do that because it will repeat code (unless there's a smart way to do it without repeating code?)

What I want is for Shell to not have to know about any of the *Options classes, and for the *Options classes to all be encapsulated within each respective Command class. I know it's possible that my design is just terrible and there's a blatantly simple way of doing this; if so please show me.

I know that there must be some way to do it with reflection, but I have been having difficulty figuring out how to do it.

What can I do in order to get the runtime polymorphism that I desire?





Aucun commentaire:

Enregistrer un commentaire