Type Design Guidelines for Reusable .NET Libraries
Although they are sometimes helpful to framework developers, they are confusing to users of the framework. Sentinel values are values used to track the state of the enum, rather than being one of the values from the set represented by the enum. The following example shows an enum with an additional sentinel value used to identify the last value of the enum, and intended for use in range checks. This is bad practice in framework design.
public enum DeskType {
Circular = 1,
Oblong = 2,
Rectangular = 3,
LastValue = 3 // this sentinel value should not be here
}
public void OrderDesk(DeskType desk){
if((desk > DeskType.LastValue){
throw new ArgumentOutOfRangeException(
);
}
}
Rather than relying on sentinel values, framework developers should perform the check using one of the real enum values.
public void OrderDesk(DeskType desk){
if(desk > DeskType.Rectangular || desk < DeskType.Circular){
throw new ArgumentOutOfRangeException(
);
}
}
|
DO provide a value of zero on simple enums.
Consider calling the value something like None. If such value is not appropriate for this particular enum, the most common default value for the enum should be assigned the underlying value of zero.
public enum Compression {
None = 0,
GZip,
Deflate,
}
public enum EventType {
Error = 0,
Warning,
Information,
}
CONSIDER using Int32 (the default in most programming languages)
as the underlying type of an enum unless any of the following is true:
- The enum is a flags enum and you have more than 32 flags, or expect to have more in the future.
- The underlying type needs to be different than Int32 for easier interoperability with unmanaged code expecting different size enums.
|
|
- A smaller underlying type would result in substantial savings in space. If you expect for enum to be used mainly as an argument for flow of control, the size makes little difference. The size savings might be significant if:
- You expect to the enum to be used as a field in a very frequently instantiated structure or class.
- You expect users to create large arrays or collections of the enum instances.
- You expect a large number of instances of the enum to be serialized.
For in-memory usage, be aware that managed objects are always DWORD aligned so you effectively need multiple enums or other small structures in an instance to pack a smaller enum with to make a difference, as the total instance size is always going to be rounded up to a DWORD.
DO name flag enums with plural nouns or noun phrases and simple
enums with singular nouns or noun phrases.
DO NOT extend System.Enum directly.
System.Enum is a special type used by the CLR to create user-defined
enumerations. Most programming languages provide a programming
element that gives you access to this functionality. For example, in C#
the enum keyword is used to define an enumeration.
|
Designing Flag Enums
DO apply the System.FlagsAttribute to flag enums. Do not apply
this attribute to simple enums.
[Flags]
public enum AttributeTargets {
}
DO use powers of two for the flags enum values so they can be freely
combined using the bitwise OR operation.
[Flags]
public enum WatcherChangeTypes {
Created = 0x0002,
Deleted = 0x0004,
Changed = 0x0008,
Renamed = 0x0010,
}
CONSIDER providing special enum values for commonly used combinations
of flags.
Bitwise operations are an advanced concept and should not be required for simple tasks. FileAccess.ReadWrite is an example of such a special value.
[Flags]
public enum FileAccess {
Read = 1,
Write = 2,
ReadWrite = Read | Write
}
|
DO apply the System.FlagsAttribute to flag enums. Do not apply
this attribute to simple enums.
[Flags]
public enum AttributeTargets {
}
DO use powers of two for the flags enum values so they can be freely
combined using the bitwise OR operation.
[Flags]
public enum WatcherChangeTypes {
Created = 0x0002,
Deleted = 0x0004,
Changed = 0x0008,
Renamed = 0x0010,
}
CONSIDER providing special enum values for commonly used combinations
of flags.
Bitwise operations are an advanced concept and should not be required
for simple tasks. FileAccess.ReadWrite is an example of such a special
value.
[Flags]
public enum FileAccess {
Read = 1,
Write = 2,
ReadWrite = Read | Write
}
AVOID creating flag enums where certain combinations of values are
invalid.
The System.Reflection.BindingFlags enum is an example of an incorrect design of this kind. The enum tries to represent many different concepts, such as visibility, staticness, member kind, and so on.
[Flags]
public enum BindingFlags {
Instance,
Static,
NonPublic,
Public,
CreateInstance,
GetField,
SetField,
GetProperty,
SetProperty,
InvokeMethod,
}
Certain combinations of the values are not valid. For example, the Type.GetMembers method accepts this enum as a parameter but the documentation for the method warns users, "You must specify either BindingFlags.Instance or BindingFlags.Static in order to get a return." Similar warnings apply to several other values of the enum.
If you have an enum with this problem, you should separate the values of the enum into two or more enums or other types. For example, the Reflection APIs could have been designed as follows:
[Flags]
public enum Visibilities {
Public,
NonPublic
}
[Flags]
public enum MemberScopes {
Instance,
Static
}
[Flags]
public enum MemberKinds {
Constructor,
Field,
PropertyGetter,
PropertySetter,
Method,
}
public class Type {
public MemberInfo[] GetMembers(MemberKinds members,
Visibilities visibility,
MemberScopes scope);
}
AVOID using flag enum values of zero, unless the value represents "all
flags are cleared" and is named appropriately as prescribed by the following
guideline.
The following example shows a common implementation of a check that programmers use to determine if a flag is set (see the if-statement below). The check works as expected for all flag enum values except the value of zero, where the Boolean expression always evaluates to true.
[Flags]
public enum SomeFlag {
ValueA = 0, // this might be confusing to users
ValueB = 1,
ValueC = 2,
ValueBAndC = ValueB | ValueC,
}
SomeFlag flags = GetValue();
if ((flags & SomeFlag.ValueA) === SomeFlag.ValueA) {
}
if (Foo.SomeFlag == 0) We support this special conversion to provide programmers with a consistent way of writing the default value of an enum type, which by CLR decree is "all bits zero" for any value type. |
DO name the zero-value of flag enums None. For a flag enum, the value
must always mean "all flags are cleared."
[Flags]
public enum BorderStyle {
Fixed3D = 0x1,
FixedSingle = 0x2,
None = 0x0
}
if (foo.BorderStyle == BorderStyle.None)....
However, notice that this rule only applies to flag enumerations; in the case where enumeration is not a flag enumeration, there is a real disadvantage to avoiding zero that we have discovered. All enumerations begin their life with this value (memory is zeroed by default). Thus if you avoid zero, every enumeration has an illegal value in it when it first starts its life in the run time (we can't even pretty print it properly). This seems bad. In my own coding, I do one of two things for nonflag enumerations. If there is an obvious default that is highly unlikely to cause grief if programmers forget to set it (program invariants do not depend on it), I make this the zero case. Usually this is the most common value, which makes the code a bit more efficient (it is easier to set memory to 0 than to any other value). If no such default exists, I make zero my "error" (none-of-the-above) enumeration value. That way when people forget to set it, some assert will fire later and we will find the problem. In either case, however, from the compiler (and runtime), point of view, every enumeration has a value for 0 (which means we can pretty print it). |

RICO MARIANI You can get yourself into a lot of trouble by trying to
be too clever with enums. Sentinel values are a great example of this: People
write code like the above but using the sentinel value LastValue instead
of Rectangular as recommended. When a new value comes along and
LastValue is updated, their program "automatically" does the right thing
and accepts the new input value without giving an ArgumentOutOf-
RangeException. That sounds grand except for all that we didn't show,
the part that's doing the actual work, and might not yet expect or even handle
the new value. The less clever tests will force you to revisit all the right
places to ensure that the new value really is going to work. The few minutes
you spend visiting those call sites will be more than repaid in time you save
avoiding bugs.