C# 在运行时创建子类对象的更好方法,而不是每个可能选项的开关

C# Better way to create objects of a subclass in runtime other then a switch for every possible option

我正在使用 C# 并尝试创建一种方法来解析文件,其中每一行都是我拥有的程序的新命令。

我的文件看起来像这样:

现在我只有两个可能的指令,移动停止,它们有不同的参数。

我正在尝试对我的代码进行未来验证并使其尽可能通用,因为我最终会在未来添加更多命令。

所以我决定有一个抽象的 class Command 实现 Update() 方法,然后这些命令继承自 Command 并指定 Update 的作用。

这个命令最终出现在一个列表中,我为列表中的每个元素调用更新。

(这可能不是最好的实现,请随意提出更好的实现)

现在我必须读取文件并解析此命令,这意味着在运行时创建新的 Move 或 Stop 对象,但我必须知道必须创建哪个对象的唯一方法是在读取文件行时。

我知道我可以为第一个单词使用一个简单的 switch 语句来做到这一点,但这使得我每次有新命令时都必须手动添加一个新的 case 块。

有没有更好的方法在运行时实例化给定子class的对象,而不是为每个可能的选项进行切换?

提前致谢!

在您的语法始终为 [Command] [Space separated numeric argument list] 的简单场景中,您可以考虑以下方法:

实现一个Dictionary<string, Func<IEnumerable<int>, Command>>,其中键是命令的保留字,值是一个委托,它将接受解析后的字符串参数(我冒昧地假设它们都是整数)并构建适当的命令。这些委托可以指向在 Command 中实现的静态工厂方法,类似于 Linq.Expressions 的构建方式:

public abstract class Command
{
     private const int validMoveArgumentCount = 2;
     private const int validStopArgumentCount = 1;

     public static Command Move(IEnumerable<int> args)
     {
         if (args == null)
             throw new ArgumentNullException(nameof(args));

         var arguments = args.ToArray();
         var argumentCount = arguments.Length;

         if (argumentCount != validMoveArgumentCount)
             throw new SyntaxErrorException("Invalid number of arguments in Move command. Expected {validMoveArgumentCount}, received {argumentCount}.");

         return new MoveCommand(arguments[0], arguments[1]);
     }

     public static Command Stop(IEnumerable<int> args)
     {
         if (args == null)
             throw new ArgumentNullException(nameof(args));

         var arguments = args.ToArray();
         var argumentCount = arguments.Length;

         if (argumentCount != validStopArgumentCount)
             throw new SyntaxErrorException("Invalid number of arguments in Stop command. Expected {validStopArgumentCount}, received {argumentCount}.");

         return new StopCommand(arguments[0]);
     }

     public abstract void Update();
     ...

     private class MoveCommand: Command { ... }
     private class StopCommand: Command { ... }
}

请注意我非常喜欢的模式;嵌套的私有 class 派生自包含的抽象 class。这是一种完全隐藏具体命令的实现并精确控制谁可以实例化它们的巧妙方法。

您需要有一个初始化方法来构建字典,大致如下:

var commandDictionary = new Dictionary<string, Func<IEnumerable<int>, Command>>();
commandDictionary["move"] = args => Command.Move(args);
commandDictionary["stop"] = args => Command.Stop(args);

这基本上是您连接解析逻辑的地方。

现在,在解析你输入的每一行命令时,你会做类似于(过于简单化)的事情:

private Command ParseCommand(string commandLine)
{
    Debug.Assert(!string.IsNullOrWhiteSpace(commandLine));

    var words = commandLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

    if (commandDictionary.ContainsKey(words[0]))
        return commandDictionary[words[0]](words.Skip(1).ParseIntegerArguments());

    throw new SyntaxErrorException($"Unrecognized command '{words[0]}'.");
}

private static IEnumerable<int> ParseIntegerArguments(IEnumerable<string> args)
{
     Debug.Assert(args != null);

     foreach (var arg in args)
     {
         int parsedArgument;

         if (!int.TryParse(arg, out parsedArgument))
             throw new SyntaxErrorException("Invalid argument '{arg}'");

         yield return parsedArgument;
     }
}

综上所述,这里您没有使用 switch 语句,而是在构建字典。归根结底,您必须以某种方式定义哪些命令有效以及您将如何处理它们。也许使用这种方法,添加新命令会更简洁一些,但这取决于个人口味。

还值得一提的是,为了简单起见,我在遇到不正确的语法时抛出异常。如果我认真地实现一个类似的解析器,我就不会这样做,因为正如 Eric Lippert 所说,我会特别抛出 vexing exceptions。我可能只是尽可能地解析整个事情(在这种特殊情况下错误恢复非常微不足道)在适当的时候将描述性错误对象添加到 diagnosticsBag (一些 IList<ParsingError>) 然后生成一个聚合解析错误报告。