通过一种方法实例化各种继承的类,无需反射

本文关键字:继承 反射 实例化 方法 一种 | 更新日期: 2023-09-27 18:35:33

在我正在从事的一个项目中,我有一组块构成了一个基于3D体素的环境(如Minecraft)。这些世界存储在外部数据文件中。

此文件包含以下各项的数据:每个块,它的位置,及其类型。

调用 LoadLevel 方法时,我希望它遍历文件中每个块的数据,为每个块创建一个 Block 对象的新实例。传递位置之类的东西是没有问题的。就像

CreateBlock(Vector3 position)

问题出在类型上。所有类型都是子类(想想抽象块,然后是继承抽象块属性的子类型,如 GrassBlock 或 WaterBlock。假设有一个像"GrassBlock"这样的子类,而不是一个泛型块,我如何通过该方法让它做到这一点?我知道的唯一方法是通过反思,我被建议远离反思。我可以通过泛型类型或其他方式做到这一点吗?

这似乎是游戏设计中一个如此重要的问题,但我问过的人似乎都不知道。有什么帮助吗?

通过一种方法实例化各种继承的类,无需反射

型类型仍然需要反射。

首先:您正在寻找的是工厂模式。它为您创建对象,而不必在任何地方明确自己执行此操作。

基本上有两种选择:

  • 反射

这确实具有与之相关的性能影响,但如果您尚未确定它是一个问题,请不要忽略它。它将是可读的和维护的。

  • 开关

对每个选项进行硬编码,并根据您传入的某种元数据创建一个新实例(将标识每个块的类型)。这样做的好处是不使用反射,因此不会造成性能损失,但它的可扩展性也会降低,如果你有 500 个不同的块,你可以猜测你的代码会是什么样子。

当然,您可以在没有任何反射的情况下创建对象。简单地为每个类分配整数索引:

Func<Vector3, Block>[] factories =
{
    (v) => new GrassBlock(v),    // index 0
    (v) => new WaterBlock(v),    // index 1
    . . .
}

将此索引保存在外部数据中。在反序列化时读取 Vector3 v并索引i,然后调用 var block = factories[i](v);

如果没有反射,您可以使用工厂方法,带有switch .假设BlockType是一个枚举。

public static Block CreateBlock(BlockType type, Vector3 position)
{
    switch (BlockType type)
    {
    case BlockType.Grass:
        return new GrassBlock(position);
    case BlockType.Water:
        return new WaterBlock(position);
    default:
        throw new InvalidOperationException();
    }
}

但是为了拥有更易于维护的东西,你仍然可以使用反射,直到它被证明是一个瓶颈。在这种情况下,您可以切换到运行时代码生成

private static readonly Dictionary<Type, Func<Vector3, Block>> _activators = new Dictionary<Type, Func<Vector3, Block>>();
public static Block CreateBlock(Type blockType, Vector3 position)
{
    Func<Vector3, Block> factory;
    if (!_activators.TryGetValue(blockType, out factory))
    {
        if (!typeof(Block).IsAssignableFrom(blockType))
            throw new ArgumentException();
        var posParam = Expression.Parameter(typeof(Vector3));
        factory = Expression.Lambda<Func<Vector3, Block>>(
            Expression.New(
                blockType.GetConstructor(new[] { typeof(Vector3) }),
                new[] { posParam }
            ),
            posParam
        ).Compile();
        _activators.Add(blockType, factory);
    }
    return factory(position);
}

此代码将在运行时生成工厂函数,这是第一次请求给定类型的块时。如果需要,您可以使用 ConcurrentDictionary 使此函数线程安全。

但对于您的目的来说,这可能有点矫枉过正;)

你为什么要避免反思?如果您只能在启动时执行此代码(听起来如果您正在读取文件,则可以执行),那么我个人在使用反射方面没有太大问题。

另一种方法是存储完全限定的类型名称(例如 My.System.Blocks.GrassBlock)并加载该类型。

var typeName = readStringTypeFromFile(file);
Block type = Activator.CreateInstance(typeName, location);

正如我所说,在启动时运行这样的东西对我来说很好,如果需要,您可以测试它的性能。

快速而肮脏的小提琴:https://dotnetfiddle.net/BDmlyi