2
\$\begingroup\$

So I had this idea; what if I could decorate my Enum values with an attribute that cross-references a "default" value of another Enum that should be used when a variable of the current Enum type has this value?

The desired usage would look like:

public enum MyEnum
{
    Value1,
    Value2,
    Value3,
}

public enum ControllingEnum
{
    [LinkedValue(MyEnum.Value1)]
    ControllingValue1,
    [LinkedValue(MyEnum.Value2)]
    ControllingValue2,
    [LinkedValue(MyEnum.Value1)]
    ControllingValue3,
    [LinkedValue(MyEnum.Value3)]
    ControllingValue4,    
}

...

var relatedValue = ControllingEnum.ControllingValue4.GetLinkedValue<MyEnum>();

I came up with the following:

public class LinkedValueAttribute:Attribute
{
    public LinkedValueAttribute(object value)
    {
        TypeOfValue = value.GetType();
        Value = (Enum)value;
    }

    public Type TypeOfValue { get; private set; }
    public Enum Value { get; private set; }
}

public static class AttributeHelper
{
    public static T GetLinkedValue<T>(this Enum enumValue) where T:struct
    {
        if (!typeof(Enum).IsAssignableFrom(typeof(T))) 
            throw new InvalidOperationException("Generic type must be a System.Enum");

        //Look for LinkedValueAttributes on the enum value
        LinkedValueAttribute[] attr = enumValue.GetType().GetField(enumValue.ToString())
            .GetCustomAttributes(typeof(LinkedValueAttribute), false)
            .OfType<LinkedValueAttribute>()
            .Where(a=>a.TypeOfValue == typeof(T))
            .ToArray();

        if (attr.Length > 0) // a DescriptionAttribute exists; use it
            return (T)(object)attr[0].Value;
    }
}

It does the job, but the boxing and unboxing are generally to be avoided. The question is, how?

\$\endgroup\$
  • \$\begingroup\$ Your GetLinkedValue method can't compile - not all code paths return a value. \$\endgroup\$ – Jesse C. Slicer Aug 22 '12 at 19:35
  • \$\begingroup\$ My guess is a last line needs to be added to the method of the nature of throw new InvalidOperationException("No cross-reference LinkedValueAttribute was found for the given value"); or even return (T)(object)enumValue; (which won't be all that useful). \$\endgroup\$ – Jesse C. Slicer Aug 22 '12 at 19:43
  • \$\begingroup\$ @JesseC.Slicer - You are correct; I caught that after posting this, and I just throw an exception. \$\endgroup\$ – KeithS Aug 22 '12 at 20:29
  • \$\begingroup\$ Oh, hey! I just remembered this and it could be helpful with what your enum work. Jon Skeet has a nifty gizmo that can constrain generics to specific enums: msmvps.com/blogs/jon_skeet/archive/2009/09/10/… \$\endgroup\$ – Jesse C. Slicer Aug 22 '12 at 21:31
2
\$\begingroup\$

I would personally write the attribute class as such:

[AttributeUsage(AttributeTargets.Field)]
public sealed class LinkedValueAttribute : Attribute
{
    private readonly Type typeOfValue;

    private readonly Enum value;

    public LinkedValueAttribute(object value)
    {
        this.typeOfValue = value.GetType();
        this.value = (Enum)value;
    }

    public Type TypeOfValue
    {
        get
        {
            return this.typeOfValue;
        }
    }

    public Enum Value
    {
        get
        {
            return this.value;
        }
    }
}

Reasons:

1: the AttributeUsage attribute limits it to going on fields, which, in this case, is the same as an enum member. Without it, the attribute can go onto anything anywhere, and while harmless from the point of the extension method, it is just a bit weird. Signify intent and don't let your users shoot themselves in the foot if you have the means.

2: sealed class. If you see no more functionality being added by subclasses in the future, seal it up. Compiler and JIT can do some optimizations knowing there are no descendants.

3: readonly fields instead of automatically implemented properties. First, signifies intent. Those fields will not be modified after the constructor assigns them. Don't let any future methods in that class noodle them unknowingly. Second, the compiler and JIT may do some optimizations based on knowing the fields will be unmolested after construction.

Now, in your extension method, since you already use LINQ, there's a couple more extensions you can use:

public static class AttributeHelper
{
    public static T GetLinkedValue<T>(this Enum enumValue) where T : struct
    {
        if (!typeof(Enum).IsAssignableFrom(typeof(T)))
        {
            throw new InvalidOperationException("Generic type must be a System.Enum");
        }

        // Look for LinkedValueAttributes on the enum value.
        var attributes = enumValue.GetType()
            .GetField(enumValue.ToString())
            .GetCustomAttributes(typeof(LinkedValueAttribute), false)
            .OfType<LinkedValueAttribute>()
            .Where(a => a.TypeOfValue == typeof(T));

        // A DescriptionAttribute exists; use it.
        if (attributes.Any())
        {
            return (T)(object)attributes.Single().Value;
        }

        throw new InvalidOperationException("No cross-reference LinkedValueAttribute was found for the given value");
    }
}

1: Removed .ToArray() since the LINQ extensions I use don't need an array to work on.

2: Changed attributes.Length > 0 to attributes.Any(). This is primarily to signify intent. You want to know if there are any attributes returned, not particularly interested in the length of the array (that's no longer needed).

3: Changed attributes[0].Value to attributes.Single().Value. Because the default for AttributeUsage is Multiple = false and that's a good thing for the semantics of how it's used - we'll only get either zero or one back.

4: Added the exception throw I mentioned in my comment.

Now, as for the box/unbox... I'm still noodling on that one. I'll update this if/when I get something.

UPDATE I'm still not finding any way around the box/unbox issue. Reflection tricks and casting sorcery is still requiring the object bit.

\$\endgroup\$
  • \$\begingroup\$ I will make the suggested improvements to the extension method. I'm not a huge proponent of using readonly behind a get-only property (especially if all data members are readonly; why not just expose the fields?), but it's definitely something that could be done. One thing: if there's more than one attribute for a type, Single will panic and throw out, but I can handle that situation easily by just returning the first one. \$\endgroup\$ – KeithS Aug 22 '12 at 20:30
  • \$\begingroup\$ But there won't be more than just one attribute for the individual field. Zero or one only. So Single() will have no opportunity to panic. \$\endgroup\$ – Jesse C. Slicer Aug 22 '12 at 21:06
  • \$\begingroup\$ ... Unless I want more than one attribute for the individual field and set the AllowMultiple property to True on the AttributeUsage tag. I want to be able to specify multiple links to enum values of other types; unfortunately it's impossible at present to limit those to one per other linked type (generic attributes FTW) \$\endgroup\$ – KeithS Aug 22 '12 at 21:07
  • \$\begingroup\$ I see your point. But that would require a pretty big breaking change in the extension method as it is (i.e. returning IEnumerable<T> instead of T) and you could make the move away from Single() at that point. Scratch the above. Not sure how the extension method would work for multiple \$\endgroup\$ – Jesse C. Slicer Aug 22 '12 at 21:14
  • \$\begingroup\$ We're searching for an attribute with a given TypeOfValue. There should be a maximum of one of these on any given enum value; however I can't enforce that. What I can do is ignore additional attributes with a value of the same type by using First(). That was my point, way back when. \$\endgroup\$ – KeithS Aug 22 '12 at 21:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.