3
\$\begingroup\$

I was thinking about how I was going to use some enums I made, how I was going to make sure they had valid values and how to call the class which would contain the necessary extensions methods. And then I thought: why not wrap the enum in a struct. I wrote up a little example code and it looks like this (ignore the fact that it's a very common day enum, I needed something as an example):

public struct Day
{
    private enum DayE
    {
        Monday = 1,
        Tuesday,
        Wednesday,
        Thursday,
        Friday
    }

    private readonly DayE value;

    public static Day Monday { get { return new Day( DayE.Monday ); } }
    public static Day Tuesday { get { return new Day( DayE.Tuesday ); } }
    public static Day Wednesday { get { return new Day( DayE.Wednesday ); } }
    public static Day Thursday { get { return new Day( DayE.Thursday ); } }
    public static Day Friday { get { return new Day( DayE.Friday ); } }

    private Day( DayE value )
    {
        this.value = value;
    }

    public override string ToString()
    {
        switch( value )
        {
            case DayE.Monday:
                return "mo";
            case DayE.Tuesday:
                return "tu";
            case DayE.Wednesday:
                return "we";
            case DayE.Thursday:
                return "th";
            case DayE.Friday:
                return "fr";
            default:
                throw new InvalidOperationException( "Can't convert 0 value" );
        }
    }
}

Some advantages of this approach I could think of:

  • Allows you to limit the possible values, only the ones you construct using the private constructor can be made. (Of course, the 0 value can always be constructed using default(Day) or new Day().) This means you only have to test if the value is not 0 to make sure it's valid. If 0 is a valid value, you don't have to test for validity.
  • No need for an helper class for the extension methods, you can just put them in the struct. Also, you can override the ToString method, as can be seen in the example.
  • In a similar fashion, static methods are now possible.
  • I think it should be equally fast to just using the enum, since the compiler will probably take away most of the wrapper. Unless, of course, you add functionality, like validity checks, to methods.

Disadvantages:

  • Need to create public static properties for all enum values. Although this does allow you to create extra values from combinations of other values, that you don't want to put in the enum itself (useful for tweaking the ToString behavior of a enum with Flags attribute).
  • Need to define methods for all common enum functionality (for each wrapper you make), like HasFlag and ==, !=, & and | operators. However, this gives you control over which of these methods/operators are available and it allows for validity checks inside them. It's possible to create one or more VS code snippets for this.
  • Visual Studio's auto complete will be a little less happy with this. With enums it usually immediately auto selects the enum type name when you type e.g. Day x =. With this you'll first have to (partially) type that.
  • To use this in a switch statement, you need to make the internal value and enum public, because structs don't work in a switch.

What do you think of this construction? Can you name any more disadvantages? Would you recommend me not to use this?

Edit: found a new disadvantage, see last point above.

\$\endgroup\$
  • \$\begingroup\$ If you want to prevent use of the type via default(Day) and force consumers to acquire instances through the predefined properties, you could make value nullable. This may or may not be desirable. \$\endgroup\$ – Aluan Haddad Feb 10 '17 at 4:36
  • 2
    \$\begingroup\$ Can you give some examples of extension methods which you are going to put in this struct? \$\endgroup\$ – Sergey Berezovskiy Feb 10 '17 at 9:36
  • \$\begingroup\$ @AluanHaddad Then you have the same problem, only with null values. Also, this will take away all the compiler optimizations which makes it close to using an enum performance-wise. \$\endgroup\$ – RdJ Feb 10 '17 at 13:11
  • \$\begingroup\$ @SergeyBerezovskiy That really depends on what you're gonna use it for, obviously. Could be things like validation, or special ways of printing the value for whatever reason. In this example you could make PrevDay() and NextDay() methods which take into account that the Monday comes after the Friday (and vice versa). These methods could actually be properties instead, which is impossible for enums. For validation, you could have different methods, e.g. if you have a Flags enum, one that validates if it's exactly one valid flag, and one that validates if it's a valid combination of flags. \$\endgroup\$ – RdJ Feb 10 '17 at 13:15
  • \$\begingroup\$ I would look hard at making enums work before I did this. \$\endgroup\$ – paparazzo Feb 10 '17 at 15:23
3
\$\begingroup\$

To use static instances of value types instead of plain enum isn't a strange thing. I can't discuss its merits because each case is different (for sure it's slightly slower but you will hardly notice that) but you can see it often (also in .NET Framework code.) Use it cum grano salis because IMO there aren't many use cases unless you want to emulate more powerful Java's enum.

What's wrong, in my opinion, is that you're mixing too many responsibilities inside the same class. Note that you also have code for representation (abbreviated enum value) which usually doesn't belong to domain.

Second point is to wrap the enum. If you use this pattern then you don't need an enum any more, just drop it and integer value (day of week) will have its own clear name.

Third point is initialization, you don't need a property, because Day is an immutable value type your code may be simplified to a static readonly field.

Let's imagine to declare your struct like this:

public struct Day
{
    public static readonly Day Monday = new Day(1);
    public static readonly Day Tuesday = new Day(2);

    private Day(int dayOfWeek)
    {
        _dayOfWeek = dayOfWeek;
    }

    private readonly int _dayOfWeek;
}

Now we have to make it easy to use during our debugging sessions (and integer value isn't so friendly). We have two options here, adding another field with long name (which eventually might hold also the short one) like this:

public static readonly Day Monday = new Day(nameof(Monday), 1);

Note that the main point is to keep everything in one single point. To add another day (!), for example Sunday, you need to change only one point in your code.

If you need it only for debugging then I'd avoid to override ToString() and I'd go with DebuggerDisplayAttribute but it's a matter of preference. ToString() is sometimes the mandatory choice for type conversion in many poor-designed libraries and I'd avoid to use it as debugging aid when I have better options.

[DebuggerDisplay("{_name}")]
public struct Day { /* */ }

Second option is to write a type ad-hoc for debugging purposes without polluting your value type with unused and heavy strings. Let's introduce DebuggerTypeProxyAttribute:

[DebuggerTypeProxy(typeof(DayDebugView))]
public struct Day { /* */ }

In DayDebugView you can put everything you need for debugging:

internal sealed class DayDebugView
{
    public DayDebugView(Day day)
    {
        _day;
    }

    public string Name
    {
        get
        {
            // Pseudo-code with some Reflection Magic
            // To get the static field name...
            return FindStaticFieldByValue(typeof(Day), _day)?.Name;
        }
    }

    private readonly Day _day;
}

Now you're done (in one way or another) with your debugging aid. If you need a presentation value then get rid of those hard-coded strings (because they're not localization friendly). Note that you might want, it depends on your usage pattern, use/implement IFormatProvider (in a separate class), implement IFormattable, implement a TypeConverter (in a separate class), implement IConvertible or use ValueConverterAttribute and implement IValueConverter. Which one is better/required depends on your UI requirements. For UI ToString() is just an abused handy method...


Add to all the above also code for comparison (at least overriding Equals() and implementing IEquatable<T> but probably also == and !=), eventually type conversions (to/from integers?), parsing to/from strings, serialization and...do you also need flags? Then few test to exercise you did everything correctly and you will ask yourself why you're not using a simple enum (with few lines of validation when it's an input.) Yes, sometimes you will need this structure but less often than you might imagine.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your lengthy answer. I realize you don't want to go through this every time you create a struct, but you could create a partial struct and use that over and over (why can't I inherit struct code...). The reason I used an enum instead of an integer for the internal value, is that it uses just one int of memory, while still allowing for usage of the enum's ToString and things like enumerating possible values. \$\endgroup\$ – RdJ Feb 10 '17 at 11:10
  • \$\begingroup\$ A partial struct is just...copy & paste (or a code snippet which can be easily renamed.) Yes, you might do it but there should be a good reason to prefer this over an enum (which anyway you won't find yourself to use too often.) If the only reason is integrity (to avoid bugs like Day day = (Day)43738) then input validation is more than enough. \$\endgroup\$ – Adriano Repetti Feb 10 '17 at 11:18
  • \$\begingroup\$ To be sealed let the compiler (and especially JIT compiler) be able to perform a more aggressive optimization and to keep them lightweight. Moreover there are other bigger differences: less overhead, ignoring boxing there isn't memory wasted for references, faster and cheaper allocation... \$\endgroup\$ – Adriano Repetti Feb 10 '17 at 11:19
  • 1
    \$\begingroup\$ Note that I am not saying "do not ever do it!". What I am saying is that it's not a 1:1 replacement for enum and you should carefully consider when to use it (if you feel you need a struct then it might also be that enum wasn't the right choice...) \$\endgroup\$ – Adriano Repetti Feb 10 '17 at 11:44
  • 1
    \$\begingroup\$ But the point I was pushing isn't performance but clarity. If somewhere I see you're assigning an enum then I immediately understand it's bla bla bla. If I see a struct then I have to go to its definition and study its code. That said I sometimes use struct instead of enum when I need something smarter (same as Java's enum, for example) \$\endgroup\$ – Adriano Repetti Feb 10 '17 at 13:35
1
\$\begingroup\$

It looks to me that since value is populated from the constructor, which can only be called with values that you control, checking for invalid values is kind of superfluous. Thus your ToString could look something like this:

public override string ToString()
{
    return value.ToString().Substring(0, 2).ToLower();
}
\$\endgroup\$
  • \$\begingroup\$ You're right, though you still need to check for 0. It was just an example, but there's all kinds of things you can do in ToString. Especially when using flags, you can do some special thing to combine names. \$\endgroup\$ – RdJ Feb 10 '17 at 11:05
0
\$\begingroup\$

If you wanted to keep an enum while having some extra features, at some marginal cost you could consider decorating it with custom attributes, to mimic your requirements of your ToString()

public enum Day
{
    [ShortString("mo")]
    Monday = 1,
    [ShortString("tu")]
    Tuesday,
    [ShortString("we")]
    Wednesday,
    [ShortString("th")]
    Thursday,
    [ShortString("fr")]
    Friday
}

public class ShortStringAttribute : Attribute
{
    public ShortStringAttribute(string value)
    {
        // Do stuff with it.   
    }
}
\$\endgroup\$
  • \$\begingroup\$ This is an option, if that's the only thing you want. However, this takes away all the other advantages and does not work with methods like string.Format. \$\endgroup\$ – RdJ Feb 10 '17 at 17:14
  • \$\begingroup\$ Well, you could create an Attribute to handle other scenarios as well. There really isn't much limitations on how far you can go with attributes. :) \$\endgroup\$ – Svek Feb 10 '17 at 17:20

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.