如何在 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'>}
具体问题
所以最终这归结为三个相互关联的问题:
我应该给 MathOperations
分配什么样的类型,以便它可以保存 GenericOperation
的子类型的集合?
如何获取 GenericOperation
的子类型?
我分配什么类型 operation
,可以是几种类型之一?
到目前为止工作
我一直在从以下一些来源研究泛型和反射,但到目前为止 none 似乎提供了我正在寻找的信息。
- https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics
- https://igoro.com/archive/fun-with-c-generics-down-casting-to-a-generic-type/
- Using enum as generic type parameter in C#
- 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 上输入的那样,但我希望这对您的用例有所帮助,并让您了解我将如何处理此问题
背景:
我正在为 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'>}
具体问题 所以最终这归结为三个相互关联的问题:
我应该给
MathOperations
分配什么样的类型,以便它可以保存GenericOperation
的子类型的集合?如何获取
GenericOperation
的子类型?我分配什么类型
operation
,可以是几种类型之一?
到目前为止工作
我一直在从以下一些来源研究泛型和反射,但到目前为止 none 似乎提供了我正在寻找的信息。
- https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics
- https://igoro.com/archive/fun-with-c-generics-down-casting-to-a-generic-type/
- Using enum as generic type parameter in C#
- 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 上输入的那样,但我希望这对您的用例有所帮助,并让您了解我将如何处理此问题