dcsimg
September 26, 2017
Hot Topics:

Type Design Guidelines for Reusable .NET Libraries

  • December 23, 2005
  • By Krzysztof Cwalina and Brad Abrams
  • Send Email »
  • More Articles »

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(…);
}
…
}

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.

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.

BRAD ABRAMS This might not be as uncommon a concern as you first expect. We are only in version 2.0 of the .NET Framework and we are already running out of values in the CodeDom GeneratorSupport enum. In retrospect, we should have used a different mechanism for communicating the generator support options than an enum.

RICO MARIANI Did you know that the CLR supports enums with an underlying type of float or double even though most languages don't choose to expose it? This is very handy for strongly typed constants that happen to be floating point (e.g., a set of canonical conversion factors for different measuring systems). It's in the ECMA standard.

  • 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.

BRAD ABRAMS Keep in mind that it is a binary breaking change to change the size of the enum type once you have shipped, so choose wisely with an eye on the future. Our experience is that Int32 is usually the right choice and thus we made it the default.

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
}

JEFFREY RICHTER I use flag enums quite frequently in my own programming. They store very efficiently in memory and manipulation is very fast. In addition, they can be used with interlocked operations, making them ideal for solving thread synchronization problems. I'd love to see the System.Enum type offer a bunch of additional methods that could be easily inlined by the JIT compiler that would make source code easier to read and maintain. Here are some of the methods I'd like to see added to Enum: IsExactlyOneBitSet, CountOnBits, AreAllBitsOn, AreAnyBitsOn, and TurnBitsOnOff.

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) {
…
}

ANDERS HEJLSBERG Note that in C# the literal constant 0 implicitly converts to any enum type, so you could just write:

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)....

VANCE MORRISON The rational for avoiding zero in a flag enumeration for an actual flag (only the special enum member None should have the value zero) is that you can't OR it in with other flags as expected.

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).





Page 5 of 6



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date
Rocket Fuel