如何在 C# 中使用基于运行时动态类型的泛型调度替换枚举上的 switch 语句?

How do I replace a switch statement over an enum with runtime-dynamic type-based generic dispatch in C#?

背景:

我正在为 Unity 构建一个编辑器扩展(虽然这个问题严格来说与统一无关)。用户可以 select 从下拉列表中进行二元运算,并在输入上执行该运算,如图所示:

代码取自教程,并使用枚举 here in combination with a switch statement here 实现所需的行为。

下一张图片演示了图中代码与行为之间的关系 UI:

问题

根据我以前使用其他语言编程的经验,以及我希望允许用户进行不需要用户在核心代码中编辑 switch 语句的可扩展操作,我希望生成的代码看起来像像这样(无效的)C# 代码:

... snip ...

        // OperatorSelection.GetSelections() is automagically populated by inheritors of the GenericOperation class
        // So it would represent a collection of types?
        // so the confusion is primarily around what type this should be
        public GenericOperations /* ?? */ MathOperations = GenericOperation.GetOperations();

        // this gets assigned by the editor when the user clicks 
        // the dropdown, but I'm unclear on what the type should
        // be since it can be one of several types
        // from the MathOperations collection
        public Operation /* ?? */ operation;
        
        public override object GetValue(NodePort port)
        {
            float a = GetInputValue<float>("a", this.a);
            float b = GetInputValue<float>("b", this.b);
            result = 0f;
            result = operation(a, b);
            return result;
        }

... snip ...

参考行为 为了 crystal 清楚我希望实现的行为类型,这里是 Python.

中的参考实现
class GenericOperation:

    @classmethod
    def get_operations(cls):
        return cls.__subclasses__()


class AddOperation(GenericOperation):

    def __call__(self, a, b):
        return a + b


if __name__ == '__main__':
    op = AddOperation()
    res = op(1, 2)
    print(res)  # 3
    print(GenericOperation.get_operations())  # {<class '__main__.AddOperation'>}

具体问题 所以最终这归结为三个相互关联的问题:

  1. 我应该给 MathOperations 分配什么样的类型,以便它可以保存 GenericOperation 的子类型的集合?

  2. 如何获取 GenericOperation 的子类型?

  3. 我分配什么类型 operation,可以是几种类型之一?

到目前为止工作

我一直在从以下一些来源研究泛型和反射,但到目前为止 none 似乎提供了我正在寻找的信息。

  1. https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics
  2. https://igoro.com/archive/fun-with-c-generics-down-casting-to-a-generic-type/
  3. Using enum as generic type parameter in C#
  4. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generics-and-reflection

编辑:我编辑了 C# 伪代码中的注释,以反映主要的混淆归结为 MathOperations 和 [=15= 的类型应该是什么],并注意当用户单击下拉菜单时,编辑器本身 select 是 MathOperations 中的 operation。我也改了问题,让他们如实回答。

通常我会说你的问题很广泛,用例非常棘手,需要很多不那么简单的步骤来处理。但我看到您在研究和您的问题上也付出了很多努力,所以我会尝试做同样的事情(小圣诞礼物);)

总的来说,我认为泛型不是您想在这里使用的。泛型总是需要编译时常量参数。

因为我只是在 phone 并且不知道我现在不能给你一个完整的解决方案,但我希望我能把你带入正确的轨道。


1。公共接口或基础 class

我认为最简单的事情宁愿是一个常见的interface例如

public interface ITwoFloatOperation
{
    public float GetResult(float a, float b);
}

一个普通的abstract基地class当然也可以。 (你甚至可以在方法上使用某个属性)

然后有一些实现,例如

public class Add : ITwoFloatOperation
{
    public float GetResult(float a, float b) => a + b;
}

public class Multiply : ITwoFloatOperation
{
    public float GetResult(float a, float b) => a * b;
}

public class Power : ITwoFloatOperation
{
    public float GetResult(float a, float b) Mathf.Pow(a, b);
}

... etc

2。使用 Reflection

查找所有实现

然后您可以使用 Reflection (you already were on the right track there) in order to automatically find all available implementations of that interface like e.g. this

using System.Reflection;
using System.Linq;

...

var type = typeof(ITwoFloatOperation);
var types = AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(s => s.GetTypes())
    .Where(p => type.IsAssignableFrom(p));

3。 Store/Serialize Unity 中的选定类型

现在您拥有所有类型...

但是,为了在 Unity 中真正使用它们,您将需要一个额外的特殊 class,即 [Serializable] 并且可以存储类型,例如喜欢

[Serializable]
// See https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.html
public class SerializableType : ISerializationCallbackReceiver
{
    private Type type;
    [SerializeField] private string typeName;

    public Type Type => type;

    public void OnBeforeSerialize()
    {
        typeName = type != null ? type.AssemblyQualifiedName : "";
    }

    public void OnAfterDeserialize()
    {
        if(!string.NullOrWhiteSpace(typeName)) type = Type.GetType(typeName);
    }
}

4。界面类型选择和绘制下拉

然后,由于您不想手动键入名称,因此您需要一个特殊的下拉菜单抽屉,其中包含实现您的界面的给定类型(您看到我们正在连接点)。

我可能会使用像

这样的属性
[AttributeUsage(AttributeTarget.Field)]
public ImplementsAttribute : PropertyAttribute
{
    public Type baseType;

    public ImplementsAttribute (Type type)
    {
        baseType = type;
    }
}

然后您可以公开该字段,例如

[Implements(typeof (ITwoFloatOperation))]
public SerializableType operationType;

然后有一个自定义抽屉。这当然取决于您的需求。老实说,我的编辑器脚本知识更多地基于 MonoBehaviour 等,所以我只希望你能以某种方式将其转化为你的图表。

例如

 [CustomPropertyDrawer(typeof(ImplementsAttribute))]
public class ImplementsDrawer : PropertyDrawer
{
    // Return the underlying type of s serialized property
    private static Type GetType(SerializedProperty property)
    {
        // A little bit hacky we first get the type of the object that has this field
        var parentType = property.serializedObject.targetObject.GetType();
        // And then once again we use reflection to get the field via it's name again
        var fi = parentType.GetField(property.propertyPath);
        return fi.FieldType;
    }

    private static Type[] FindTypes (Type baseType)
    {
        var type = typeof(ITwoFloatOperation);
        var types = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(s => s.GetTypes())
            .Where(p => type.IsAssignableFrom(p));

        return types.OrderBy(t => t.AssemblyQualifiedName).ToArray();
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {   
        label = EditorGUI.BeginProperty(position, label, property);

        var implements = attribute as ImplementsAttribute;

        if (GetType(property) != typeof (SerializableType))
        {
            EditorGUI.HelpBox(position, MessageType.Error, "Implements only works for SerializableType!");
            return;
        }

        var typeNameProperty = property.FindPropertyRelative("typeName");

        var options = FindTypes (implements.baseType);

        var guiOptions = options.Select(o => o.AssemblyQualifiedName).ToArray();

        var currentType = string.IsNullOrWhiteSpace(typeNameProperty.stringValue) ? null : Type.GetType(typeNameProperty.stringValue);

        var currentIndex = options.FindIndex(o => o == curtentType);

        var newIndex = EditorGUI.Popup(position, label.text, currentIndex, guiOptions);

        var newTypeName = newIndex >= 0 ? options[newIndex] : "";
       
        property.stringValue = newTypeName;   
        EditorGUI.EndProperty();  
    }
}

5。使用类型创建实例

一旦您以某种方式可以存储并获取所需的类型作为最后一步,我们就想使用它 ^^

同样,解决方案是反射,Activator which allows us to create an instance of any given dynamic type using Activator.CreateInstance

所以一旦你有了这个领域,你就会,例如做

var instance = (ITwoFloatOperation) Activator.CreateInstance(operationType.Type));
var result = instance.GetResult(floatA, floatB);

一旦所有这些都设置好并正常工作 (^^),您的“用户”/开发人员可以添加新的操作,就像实现您的界面一样简单。




替代方法 - “脚本化行为”

进一步考虑,我想我有另一种方法——也许更简单一些。

此选项可能不是您最初的目标,也不是下拉菜单,但我们宁愿简单地使用资产的现有对象选择弹出窗口!


你可以使用我喜欢称之为“脚本化行为”的东西,并有一个像

这样的基础ScriptableObject
public abstract class TwoFloatOperation : ScriptableObject
{
    public abstract float GetResult(float a, float b);
}

然后是多个实现(注意:所有这些都必须在不同的文件中!)

[CreateAssetMenu (fileName = "Add", menuName = "TwoFloatOperations/Add")]
public class Add : TwoFloatOperation
{
    public float GetResult(float a, float b) => a + b;
}

[CreateAssetMenu (fileName = "Multiply", menuName = "TwoFloatOperations/Multiply")]
public class Multiply : TwoFloatOperation
{
    public float GetResult(float a, float b) => a * b;
}

[CreateAssetMenu (fileName = "Power", menuName = "TwoFloatOperations/Power"]
public class Power : TwoFloatOperation
{
    public float GetResult(float a, float b) Mathf.Pow(a, b);
}

然后你为每个对象创建一个实例 ProjectView -> 右击 -> Create -> TwoFloatOperations

一旦你为每种类型做了这个,你就可以简单地公开一个类型的字段

public TwoFloatOperation operation;

并让 Unity 完成所有反射工作以查找在资产中实现此功能的实例。

您只需单击对象字段旁边的小点,Unity 就会为您列出所有可用选项,您甚至可以使用搜索栏按名称查找。

优点:

  • 不需要肮脏、昂贵且容易出错的反射
  • 基本上都是基于编辑器的内置功能 -> 不用担心序列化等问题

缺点:

  • 这与 ScriptableObject 背后的实际概念略有不同,因为通常会有多个具有不同设置的实例,而不仅仅是一个
  • 如您所见,您的开发人员不仅必须继承某种类型,还必须另外添加 CreateAssetMenu 属性并实际创建一个实例才能使用它。

正如在 phone 上输入的那样,但我希望这对您的用例有所帮助,并让您了解我将如何处理此问题