【BNode】工具篇2:元类型MetaType

前言

虽然这个节点编辑器项目在很多地方参考Blender的节点系统进行设计,但在设计需求上还是有一些根本区别。Blender主要服务对象是艺术家等非程序人员,而BNode作为一个节点编辑器框架,主要服务对象是那些程序开发者。

例如,Blender中的内置类型(BasicType)数量并不多,有些地方就直接硬编码或着只为内置类型考虑了。而BNode则更多情况下还需要考虑兼容用户自定义的类型,所以注重于减少用户开发负担,例如在不少功能上都有提供默认实现或简单易用的api。

有些小特性不知道是否需要保留的时候,我就一直问小AI这个设计可能有什么好处,如果不能让我信服就删掉它。

用途和设计思路

MetaType是对System.Type在功能上的扩展,对标于Blender中的CPPType。它除了包含Type本身以外,还包含了在各种情况下如何处理类型的逻辑。例如,如何序列化/反序列化一个类型,如何拷贝、销毁(如果需要)一个类型等等。这样就可以在不修改某个类本身的情况下,为其间接扩展一些所需的功能。同时也允许多种类型共用同一种处理方式,而无需重复编写代码。

    /// <summary>
    /// “元类型”是对System.Type功能的扩展,除了类型本身以外,它还包含了在各种情况下处理类型的逻辑。
    /// <para>MetaType实现的功能接口,只代表MetaType为类型实现了这些功能,不能说明类型本身就包含这些功能</para>
    /// <para>MetaType必须有无参构造函数</para>
    /// </summary>
    public interface IMetaType
    {
        /// <summary>
        /// 此MetaType代表的最终闭合数据类型
        /// </summary>
        public Type DataType { get; }
    }

这类似于为类型扩展接口的设想,在C#的前沿也有不少呼声。但还是比较遥远,更何况是我们这些这些用Unity的,可能要到2040年才能落地吧。

表示处理逻辑

设计的关键点一是以何种方式表示这些处理逻辑。比较容易想到的是通过重载/实现方法体,或着通过委托进行存储。方法体允许让MetaType本身保持无状态,更有利于进行序列化和组合。使用委托则很难被序列化,但可以拥有更高的自由度。

理论上每个类型都需要对应的MetaType。Blender采用了类似于委托的方法,此项目则采用了接口,因为通过组合可以减少编写MetaType的时间,并且保持无状态也有利于Unity序列化。

    //以下接口都是为MetaType准备的,不要为数据类实现它们。
    //它们只代表MetaType为类型实现的功能,不能说明类型本身就包含这些功能

    /// <summary>
    /// 允许无参构造
    /// </summary>
    public interface INonParameterConstructable:IMetaType
    {
        public object ConstructData();
    }
    /// <summary>
    /// 允许拷贝构造
    /// </summary>
    public interface ICopyConstructable : IMetaType
    {
        public object CopyConstructData(object src);
    }
    /// <summary>
    /// 可以拷贝的类型
    /// </summary>
    public interface ICopyable : IMetaType
    {
        /// <summary>
        /// 绝大多数情况下,浅拷贝使用同一种实现,即为各成员直接赋值
        /// </summary>
        public object ShallowCopy(object src);
        /// <summary>
        /// 创建一个深拷贝委托。写成此种形式是为了加速利用IMetaType进行深拷贝。它与默认深拷贝的性能相近。
        /// </summary>
        public Func<object,object> DeepCopy(CopyContext context);
    }

扩展性

关键点二是应该如何支持MetaType的扩展性。随着使用场景增多,MetaType可能需要实现的逻辑也会越来越多。Blender的CPPType,有接近四十个方法可以实现。而我希望MetaType能保持简洁。

Blender为每个内置类型都注册了一个CPPType,我采用了类似的注册设计,但提供了更多的默认实现,并允许为同一个类型注册多个MetaType。这样用户就可以间接为我的甚至是C#原生的那些类型进行功能扩展了,也可以在不同使用环境下获取相应的MetaType(例如仅Unity编辑器环境下,获得实现了编辑器扩展功能的MetaType)。每个MetaType实例都实现了一个或多个的接口,并最终通过这些接口去访问扩展的功能。

因为没有优先级机制,在获取某个类型的MetaType时,会返回第一个满足给定条件的实例。因此,如果某个类型存在多个满足条件的MetaType,并且有不同的实现,可能不会返回用户预期的那一个。可以尝试给定更严格的查询条件约束,或着使用新的接口进行隔离。为了减少这种情况的发生,自定义MetaType在非必要的情况下不要去实现不相关的接口,遵循项目或用户自定义的默认MetaType行为即可。

非具体类型

手动为每个类型都编写对应的MetaType不太现实,例如对泛型类型的不同闭合。因此可以为非具体的类型设计MetaType。它允许包含一个只读的System.Type字段以在初始化时保存最终具体类型,并且必须实现一个用于具体化的接口,仍然符合无状态的定义。

    /// <summary>
    /// 描述一种可能非具体的类型,例如泛型、接口和通用(默认)类型,在使用前需要具体化
    /// </summary>
    public interface IContractualMetaType : IMetaType
    {
        /// <summary>
        /// 使用具体类型进行具体化
        /// </summary>
        IMetaType Materialize(Type concreteType);
        /// <summary>
        /// 是否已经具体化
        /// </summary>
        bool IsMaterialized { get; }
    }

为了减少代码量,经常用同一种MetaType既描述非具体类型,又描述其具体类型。因此保留一个bool属性方便进行区分。例如说,只需要编写IListMetaType,其既可以处理IList,也可以处理实现IList的具体类型。

泛型类型

可以为开放的泛型类型(形如未填入泛型参数的List<>,Dictionary<,>)设计非具体MetaType。在获取MetaType时,如果是泛型类型,会尝试通过其开放类型获取其非具体MetaType,然后再通过接口进行具体化,得到可用的具体MetaType。

接口类型

允许为接口设计非具体MetaType。与泛型类型不同的是,如果只为接口类型进行注册,通过具体实现类型无法找到对应的接口MetaType。这样设计是出于三个原因:

  • 效率低下:泛型具体类只需要查询其开放类型,接口具体类则需要遍历整个接口Map,过于复杂低效。
  • 污染实现:泛型具体类与开放类的行为一致,而接口具体类可能对接口有独立的实现。因此通过具体类查到接口MetaType很可能不符合预期。
  • 动机不足:泛型具体类难以预估和绑定,接口具体类则有限已知且容易绑定。因此具体类可以通过标签或注册直接绑定接口MetaType,不必查询。

非具体MetaType具体化后必须直接得到具体MetaType。比如说,假设为IList<T>设计一个非具体MetaType,其具体化后必须返回一个具体的、可以直接使用的MetaType,不允许分阶段具体化,例如通过int、List<int>或List<>、int来多次具体化。

通用(默认)类型

只有当找不到符合条件的MetaType时,才会尝试使用通用MetaType进行处理。通用MetaType包含了创建、拷贝和序列化等基本实现。在用户自定义的使用环境下,推荐注册对应环境的默认MetaType。

void类型

有时候需要手动指定“无”的选项。例如对于某些节点,允许包含一个自定义类型的字段,但也可以选择不包含这个字段。对“无”的特例处理始终是个麻烦的事情。既然如此,或许可以把无也当成是一种数据类型。这种特殊的MetaType,它的数据类型始终被指定为typeof(void)。然后对于“无”的处理,就可以通过指定某个具体的VoidMetaType来表示。

元类型的元类型

考虑到MetaType实际上是以接口的方式实现的,那么可以为所有MetaType写一个IMetaTypeMetaType,以此统一各个地方对于MetaType的处理。理论上,项目会为MetaType注册一个默认的IMetaTypeMetaType。但由于允许多个MetaType的设计,用户也可以为MetaType类型注册其他的实例,或着注销掉默认的实例,使用自己的实现(例如说,MetaType类型本身默认是用Newtonsoft进行序列化的,可以用此方法非破坏性地改为其他序列化实现)。

使用示例

MetaType常常与其他工具配套,作为附加信息的System.Type使用。在实际使用中,通过MetaTypeDatabase管理和隔离不同使用环境下的MetaType。每个MetaTypeDatabase实例存储当前环境下对应的各种MetaType。

public class Yu
{
     public string Str;
}
public class YuConstructMetaType : MetaTypeBase<Yu>, INonParameterConstructable
{
     public object ConstructData()=> new Yu();
}

//以项目的主要元类型库作为parent,创建一个自己的库用于专属环境
ITypeDatabase database = new MetaTypeDatabase(BCache.MainMetaTypeDB);
database.Register<Yu>(new YuConstructMetaType());
//查找Yu中可以无参构造的MetaType
var constructable = database.GetOrDefaultMetaMustAs<Yu,INonParameterConstructable>();
//通过MetaType使用扩展功能
Yu yu = constructable.ConstructData();
© 版权声明
THE END
喜欢就点个赞吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容