简体   繁体   中英

How can I use custom-attributes in C# to replace switches in switches?

I have a factory method that returns the correct sub class depending on three enum values. One way to do is, would be to use switches in switches in switches. Obviously, I don't like that option very much.

I thought that another option would be to use attributes in C#. Every sub class would have an attribute with that 3 enum values and in the factory I would only have to get the class that has the same enum values corresponding to the enum values i have in the factory.

However, I am quite new to attributes and I did not find any suitable solution in the web. If anyone, could just give me some hints or some lines of code, I really would appreciate that!

First of all, declare your attribute and add it to your classes.

enum MyEnum
{
    Undefined,
    Set,
    Reset
}

class MyEnumAttribute : Attribute
{
    public MyEnumAttribute(MyEnum value)
    {
        Value = value;
    }

    public MyEnum Value { get; private set; }
}

[MyEnum(MyEnum.Reset)]
class ResetClass
{
}

[MyEnum(MyEnum.Set)]
class SetClass
{
}

[MyEnum(MyEnum.Undefined)]
class UndefinedClass
{
}

Then, you can use this code to create a dictionary with your enums and types, and dynamically create a type.

//Populate a dictionary with Reflection
var dictionary = Assembly.GetExecutingAssembly().GetTypes().
    Select(t => new {t, Attribute = t.GetCustomAttribute(typeof (MyEnumAttribute))}).
    Where(e => e.Attribute != null).
    ToDictionary(e => (e.Attribute as MyEnumAttribute).Value, e => e.t);
//Assume that you dynamically want an instance of ResetClass
var wanted = MyEnum.Reset;
var instance = Activator.CreateInstance(dictionary[wanted]);
//The biggest downside is that instance will be of type object.
//My solution in this case was making each of those classes implement
//an interface or derive from a base class, so that their signatures
//would remain the same, but their behaviors would differ.

As you can probably notice, calling Activator.CreateInstance is not performant. Therefore, if you want to improve the performance a little bit, you can change the dictionary to Dictionary<MyEnum,Func<object>> and instead of adding types as values you would add functions wrapping the constructor of each of your classes and returning them as objects.

EDIT : I'm adding a ConstructorFactory class, adapted from this page.

static class ConstructorFactory
{
    static ObjectActivator<T> GetActivator<T>(ConstructorInfo ctor)
    {
        var paramsInfo = ctor.GetParameters();
        var param = Expression.Parameter(typeof(object[]), "args");
        var argsExp = new Expression[paramsInfo.Length];
        for (var i = 0; i < paramsInfo.Length; i++)
        {
            Expression index = Expression.Constant(i);
            var paramType = paramsInfo[i].ParameterType;
            Expression paramAccessorExp = Expression.ArrayIndex(param, index);
            Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType);
            argsExp[i] = paramCastExp;
        }
        var newExp = Expression.New(ctor, argsExp);
        var lambda = Expression.Lambda(typeof(ObjectActivator<T>), newExp, param);
        var compiled = (ObjectActivator<T>)lambda.Compile();
        return compiled;
    }

    public static Func<T> Create<T>(Type destType)
    {
        var ctor = destType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).First();
        Func<ConstructorInfo, object> activatorMethod = GetActivator<Type>;
        var method = typeof(ConstructorFactory).GetMethod(activatorMethod.Method.Name, BindingFlags.Static | BindingFlags.NonPublic);
        var generic = method.MakeGenericMethod(destType);
        dynamic activator = generic.Invoke(null, new object[] { ctor });
        return () => activator();
    }

    delegate T ObjectActivator<out T>(params object[] args);
}

You can use it as an alternative to Activator.CreateInstance , it provides greater performance if the result is cached.

var dictionary = Assembly.GetExecutingAssembly().GetTypes().
    Select(t => new { t, Attribute = t.GetCustomAttribute(typeof(MyEnumAttribute)) }).
    Where(e => e.Attribute != null).
    ToDictionary(e => (e.Attribute as MyEnumAttribute).Value, 
                 e => ConstructorFactory.Create<object>(e.t));
var wanted = MyEnum.Reset;
var instance = dictionary[wanted]();

Have a look at this article: Creating Custom Attributes . You can then use reflection (for instance GetCustomAttributes ) to get the attributes and their values.

Hope this helps

[AttributeUsage(AttributeTargets.Class)]
    public class SampleClass : Attribute {
        public SampleClass() : base() { }
        public SampleClass(YourEnum attributeValue) : this() { MyAttributeProperty = attributeValue; }
        public YourEnum MyAttributeProperty { get; set; }
    }

    public enum YourEnum { Value1, Value2, Value3 }

    [SampleClass(YourEnum.Value1)]
    public class ExampleValue1Class { }

    public class LoadOnlyClassesWithEnumValue1 {
        public LoadOnlyClassesWithEnumValue1() {

            Type[] allTypes = Assembly.GetExecutingAssembly().GetExportedTypes();
            foreach (var type in allTypes) {
                if (type.GetCustomAttributes(typeof(SampleClass), false).Length > 0) {
                    SampleClass theAttribute = type.GetCustomAttributes(typeof(SampleClass), false).Single() as SampleClass;
                    // this type is using SampleClass - I use .Single() cause I don't expect multiple SampleClass attributes, change ths if you want
                    // specify true instead of false to get base class attributes as well - i.e. ExampleValue1Class inherits from something else which has a SampleClass attribute
                    switch (theAttribute.MyAttributeProperty) {
                        case YourEnum.Value1:
                            // Do whatever
                            break;
                        case YourEnum.Value2:
                            // you want
                            break;
                        case YourEnum.Value3:
                        default:
                            // in your switch here
                            // You'll find the ExampleValue1Class object should hit the Value1 switch
                            break;
                    }
                }
            }
        }
    }

This way you can specify your enum as a parameter to the attribute. In essence, this is a very simple and lightweight DI container. I'd suggest for anything more complex, to use something like StructureMap or NInject.

Another solution would be to use Dependency Injection (DI) container. For instance using Unity DI you can:

// Register a named type mapping
myContainer.RegisterType<IMyObject, MyRealObject1>(MyEnum.Value1.ToString());
myContainer.RegisterType<IMyObject, MyRealObject2>(MyEnum.Value2.ToString());
myContainer.RegisterType<IMyObject, MyRealObject3>(MyEnum.Value3.ToString());
// Following code will return a new instance of MyRealObject1
var mySubclass = myContainer.Resolve<IMyObject>(myEnum.Value1.ToString());

Examples on using Unity: Implementing the Microsoft Unity (Dependency Injection) Design Pattern

Of course you can use any DI container (Castle Windsor, StructureMap, Ninject. Here is a list some of the available .NET DI containers List of .NET Dependency Injection Containers (IOC)

It is possible to use attributes to hold the information, but ultimately the decision process will still have to be made and will likely not be much different; just with the added complexity of the attributes. The nature of the decision remains the same regardless of where you get the information to make the decision, from the existing three enumerations or from attributes.

It may prove more fruitful to look for a way to combine the three enumerations.

Enums can be any integral type, so the easiest way to eliminate nested (and redundant) switches is to combine the enumerations together. This is easiest if the enumeration is a collection of flag values. That is, each value of the enumeration has the value of a single bit in a binary string (1 for the first bit, 2 for the second bit, 4 for the third, 8, 16 and so on).

Provided the values of each of the enumerations can be joined together it reduces the selection process to a single switch statement. This may be best done by concatenating, multiplying or adding the enumeration values -- but how they are joined together depends on the enumerations, and without knowing more details it is hard to provide more definite direction.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM