8
\$\begingroup\$

I am writing code that has objects having integer member variables where the integer value has a specific constant meaning, which normally means "use an enum".

Suppose the values are the days of the week, Monday = 0, Tuesday = 1, and so on. (In my real case, there are several different "types" involved, some of which have 10-20 possible values.)

The code uses data objects which are auto-generated, and these data objects can only have certain, already specified types (int, string, bool, double), otherwise they could just use enums themselves. The question is, should I use an enum, or a struct with static readonly definitions and implicit casting. I favor the struct version.

Here is an example of using an enum:

enum Days { Monday = 0, Tuesday = 1, Wednesday = 2, Thursday = 3, Friday = 4, Saturday = 5, Sunday = 6, } 

And here is my preferred struct code:

struct Day { readonly int day; public Day(int day) { this.day = day; } public static implicit operator int(Day value) { return value.day; } public static implicit operator Day(int value) { return new Day(value); } public static readonly Day Monday = 0; public static readonly Day Tuesday = 1; public static readonly Day Wednesday = 2; public static readonly Day Thursday = 3; public static readonly Day Friday = 4; public static readonly Day Saturday = 5; public static readonly Day Sunday = 6; } 

My code frequently involves assigning values to the members of the data object. This means if I use the struct version, I can say for example Day myDay = dataObject.Day rather than Days myDay = (Days)dataObject.Day. With the amount of logic code that will be affected by this, I think the struct version is a win for readability because it removes a lot of explicit casts.

I don't want to just use plain ints in my code because I like function signatures that indicate the purpose of the "int" being passed or returned. But I don't want the messiness that comes from casting enums all over.

I'm asking for arguments against the struct version. Would it disgust you if you found this pattern in code somewhere?


Clarification:

For the data objects to allow user types, other code which depends on the data objects would have to know about these types. The two "areas" of code are logically separate, and do not reference each other at all. I suppose a middle layer that separates the data objects from my code would be possible, but since all it would do is convert the enums to ints and back, it would be an awful lot of code to accomplish very little.

Another reason that these values sometimes must be represented as integers is that they are put through arithmetic operations sometimes. This is another place where explicit casting just gets annoying.

Finally, the definition of the possible values (days of week, in this example) is far less likely to change than the logic code. So conciseness and readability in the definition code isn't as valuable as in the logic code.

\$\endgroup\$
3
  • \$\begingroup\$What's wrong with Days.Monday? How is that not readable? Why wouldn't you want to use the DayOfWeek enumeration that's already included in the framework? I don't understand your use-case at all. It's no different than if you had used an enum.\$\endgroup\$CommentedJan 7, 2012 at 23:50
  • \$\begingroup\$Days.Monday isn't unreadable, but logic code filled with casts between Days and int is unreadable.\$\endgroup\$
    – Philip
    CommentedJan 8, 2012 at 0:14
  • \$\begingroup\$If all I needed was to define a list of constants, then of course I would prefer to use an enum, but I have run into a situation in the past where I had a set of simple enum-like values, but I also had a need to perform various kinds of operations on those values. Using a struct provided a convenient place to encapsulate the logic of those operations.\$\endgroup\$CommentedJan 10, 2012 at 22:45

4 Answers 4

4
\$\begingroup\$

Normally it would disgust me, but you appear to have a valid reason to use it, which stems from the pre-existing disgusting situation that you have to cope with these data objects that use ints.

The only thing I would ask is, why can these data objects not use enums? What is it about their auto-generation that precludes enums from being used? I do not think that enums receive any special handling at the IL level, they are handled just like primitive types are.

\$\endgroup\$
3
  • \$\begingroup\$Added clarification about why the data objects can't have user types.\$\endgroup\$
    – Philip
    CommentedJan 8, 2012 at 0:18
  • 2
    \$\begingroup\$I understand. So, the first sentence I wrote in my answer holds. And I think that in your struct you can (and probably should) declare your int day as readonly.\$\endgroup\$CommentedJan 8, 2012 at 0:23
  • \$\begingroup\$Good point about making the value readonly.\$\endgroup\$
    – Philip
    CommentedJan 8, 2012 at 0:25
4
\$\begingroup\$

Although not something I would recommend doing on a regular basis, I don't think this is necessarily a bad thing to do.

If you are going to do this, why not have the best (or is this the worst?) of both worlds? Use a private enum inside the struct to define the valid values. There are a few advantages to doing this, such as:

  1. Easy to do validation. My code (below) protects against Day x = 7;, which is invalid.

  2. Easy to implement parsing.

  3. Easy to implement ToString in such a way as to output the names of the days, as an enum would.

  4. Ability to override the Equals method to allow comparison with an Int32 value.

My version of the original code, modified to use a private enum, shown below:

struct Day { // let's make this enum private to the struct, // in order to avoid mass confusion and hysteria. enum DayValue { Monday = 0, Tuesday = 1, Wednesday = 2, Thursday = 3, Friday = 4, Saturday = 5, Sunday = 6, } readonly DayValue day; Day(DayValue day) { this.day = day; } public Day(int day) { // simple validation // Hmm, the IsDefined method causes boxing :\ if (!Enum.IsDefined(typeof(DayValue), day)) throw new ArgumentOutOfRangeException("day"); this.day = (DayValue)day; } public static implicit operator int(Day value) { return (int)value.day; } public static implicit operator Day(int value) { return new Day(value); } public static bool TryParse(string input, out Day day) { // Enum makes it easy to do parsing DayValue value; if (Enum.TryParse<DayValue>(input, out value)) { day = new Day(value); return true; } else { day = default(Day); return false; } } public override string ToString() { // Enum.ToString will provide the name of the value return this.day.ToString(); } public override bool Equals(object obj) { if (obj is int) return (DayValue)obj == this.day; return this.day.Equals(obj); } public override int GetHashCode() { return this.day.GetHashCode(); } public static readonly Day Monday = new Day(DayValue.Monday); public static readonly Day Tuesday = new Day(DayValue.Tuesday); public static readonly Day Wednesday = new Day(DayValue.Wednesday); public static readonly Day Thursday = new Day(DayValue.Thursday); public static readonly Day Friday = new Day(DayValue.Friday); public static readonly Day Saturday = new Day(DayValue.Saturday); public static readonly Day Sunday = new Day(DayValue.Sunday); } 

EDIT: Updated to provide overrides for the Equals and GetHashCode methods.

Interestingly, calls to Days.Sunday.Equals(6) and object.Equals(Days.Sunday, 6) return false, assuming that Days is an enum, even if the value of Sunday is actually 6.

A struct allows you to implement this equality logic, which might make sense considering that there are implicit conversions implemented.

\$\endgroup\$
3
  • \$\begingroup\$Using an inner enum might sound good at first, but the code is not as simple as my example using a struct. Do you actually gain anything? Bounds checking can be done even without a private enum, if desired. Just put a Min and Max variable at both ends of the list and do the check inside the constructor still.\$\endgroup\$
    – Philip
    CommentedJan 11, 2012 at 12:37
  • \$\begingroup\$@Philip - I certainly agree...the code is more complicated. I realize now that I worded my answer as if suggesting that this is better, but that was not my intention. I am for keeping it simple. Regarding bounds checking, imagine if you are mapping a set of non-consecutive integers (e.g. 3, 17, 29) to enums: now checking valid values can't be done with simply a min and max value. Also, suppose your enum changes (you add a new value). Having an inner enum just requires adding that new value to the enum; the existing validation logic will still work. Parsing is the other benefit gained.\$\endgroup\$CommentedJan 11, 2012 at 13:53
  • \$\begingroup\$Good points. In my case, parsing was unnecessary and all of the values were consecutive, but this would be a decent way to do it given those requirements.\$\endgroup\$
    – Philip
    CommentedJan 11, 2012 at 14:41
2
\$\begingroup\$

For something that has a small number of predefined values like days of the week, yes, the struct disgusts me. This is precisely what enums are for. The struct version makes sense for things with large numbers of values that may have a few predefined ones (see Color in the Framework as a good example).

\$\endgroup\$
4
  • \$\begingroup\$I do have more than seven possible values. There are also more than one of these "types", and more will potentially arise as the project continues.\$\endgroup\$
    – Philip
    CommentedJan 8, 2012 at 0:21
  • \$\begingroup\$Forgive me for asking, but what other days are there other than Monday through Sunday?\$\endgroup\$CommentedJan 8, 2012 at 2:31
  • \$\begingroup\$The days of week was an example, looks like it was a bad one. My actual code has other values.\$\endgroup\$
    – Philip
    CommentedJan 8, 2012 at 14:26
  • \$\begingroup\$Then I retract what I said. Enums are not well-suited for open-ended or gargantuan numbers of values of a type and Color becomes a very good example to follow.\$\endgroup\$CommentedJan 8, 2012 at 16:03
0
\$\begingroup\$

It would be nice if there were something "between" a struct and an enum, which would be recognized as having an integer type as its underlying representation (like enum does) but could also control what operators should be available and how they should work. One could, for example, specify that it should be possible to add an integer to a day, but not add two days together. Enum would allow both with casting, and neither without casting; a sensible type should allow the first without casting, but not the second.

\$\endgroup\$

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.