From the CLR perspective, there are only two categories of types-
reference types and value types-but for the purpose of framework
design discussion we divide types into more logical groups, each with its
own specific design rules. Figure 1 shows these logical groups.
Classes are the general case of reference types. They make up the bulk
of types in the majority of frameworks. Classes owe their popularity to the
rich set of object-oriented features they support and to their general applicability.
Base classes and abstract classes are special logical groups related
to extensibility.
Interfaces are types that can be implemented both by reference types and
value types. This allows them to serve as roots of polymorphic hierarchies
of reference types and value types. In addition, interfaces can be used to
simulate multiple inheritance, which is not natively supported by the CLR.
Structs are the general case of value types and should be reserved for
small, simple types, similar to language primitives.
Enums are a special case of value types used to define short sets of values,
such as days of the week, console colors, and so on.
Static classes are types intended as containers for static members. They
are commonly used to provide shortcuts to other operations.
Delegates, exceptions, attributes, arrays, and collections are all special
cases of reference types intended for specific uses, and guidelines for their
design and usage are discussed elsewhere in our book, Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries.
Figure 1: The logical grouping of types
DO ensure that each type is a well-defined set of related members, not
just a random collection of unrelated functionality.
It is important that a type can be described in one simple sentence. A
good definition should also rule out functionality that is only tangentially
related.
|
|
|
Types and Namespaces
Before designing a large framework you should decide how to factor your
functionality into a set of functional areas represented by namespaces. This
kind of top-down architectural design is important to ensure a coherent set
of namespaces containing types that are well integrated, don’t collide, and
are not repetitive. Of course the namespace design process is iterative and
it should be expected that the design will have to be tweaked as types are
added to the namespaces over the course of several releases. This leads to
the following guidelines.
DO use namespaces to organize types into a hierarchy of related feature
areas.
The hierarchy should be optimized for developers browsing the framework
for desired APIs.
I consider type name conflicts in a single framework to indicate sloppy |
AVOID very deep namespace hierarchies. Such hierarchies are difficult
to browse because the user has to backtrack often.
AVOID having too many namespaces.
Users of a framework should not have to import many namespaces in
most common scenarios. Types that are used together in common scenarios
should reside in a single namespace if at all possible.
|
AVOID having types designed for advanced scenarios in the same
namespace as types intended for common programming tasks.
This makes it easier to understand the basics of the framework and to
use the framework in the common scenarios.
|
|
|
|
DO NOT define types without specifying their namespaces.
This organizes related types in a hierarchy, and can help to resolve
potential type name collisions. Please note that the fact that namespaces
can help to resolve name collisions does not mean that such collisions
should be introduced.
DO NOT define types without specifying their namespaces.
This organizes related types in a hierarchy, and can help to resolve
potential type name collisions. Please note that the fact that namespaces
C#: new MyType(); IL: IL_0000: newobj instance void [MyAssembly]MyNamespace.MyType::.ctor() Notice that the C# compiler adds a reference to the assembly that defines |
|
|
Standard Subnamespace Names
Types that are rarely used should be placed in subnamespaces to avoid
cluttering the main namespaces. We have identified several groups of
types that should be separated from their main namespaces.
The .Design Subnamespace
Design-time-only types should reside in a subnamespace named .Design.
For example, System.Windows.Forms.Design contains Designers and
related classes used to do design of applications based on System.
Windows.Forms.
System.Windows.Forms.Design System.Messaging.Design System.Diagnostics.Design
DO use a namespace with the .Design suffix to contain types that provide
design-time functionality for a base namespace.
The .Permissions Subnamespace
Permission types should reside in a subnamespace named .Permissions.
DO use a namespace with the .Permissions suffix to contain types
that provide custom permissions for a base namespace.
|
The .Interop Subnamespace
Many frameworks need to support interoperability with legacy components.
Due diligence should be used in designing interoperability from the
ground up. However, the nature the problem often requires that the shape
and style of such interoperability APIs is often quite different from good
managed framework design. Thus, it makes sense to put functionality
related to interoperation with legacy components in a subnamespace.
You should not put types that completely abstract unmanaged concepts
and expose them as managed into the Interop subnamespace. It is often
the case that managed APIs are implemented by calling out to unmanaged
code. For example the System.IO.FileStream class calls out to Win32
CreateFile. This is perfectly acceptable and does not imply that the
FileStream class needs to be in System.IO.Interop namespace as
FileStream completely abstracts the Win32 concepts and publicly
exposes a nice managed abstraction.
DO use a namespace with the .Interop suffix to contain types that
provide interop functionality for a base namespace.
DO use a namespace with the .Interop suffix for all code in a Primary
Interop Assembly (PIA).
Choosing Between Class and Struct
One of the basic design decisions every framework designer faces is
whether to design a type as a class (a reference type) or as a struct (a value
type). Good understanding of the differences in the behavior of reference
types and value types is crucial in making this choice.
Reference types are allocated on the heap, and garbage-collected,
whereas value types are allocated either on the stack or inline in containing
types and deallocated when the stack unwinds or when their containing
type gets deallocated. Therefore, allocations and deallocations of value types
are in general cheaper than allocations and deallocations of reference types.
Arrays of reference types are allocated out-of-line, meaning the array
elements are just references to instances of the reference type residing on
the heap. Value type arrays are allocated in-line, meaning that the array
elements are the actual instances of the value type. Therefore, allocations
and deallocations of value type arrays are much cheaper than allocations
and deallocations of reference type arrays. In addition, in a majority of
cases value type arrays exhibit much better locality of reference.
|
Value types get boxed when cast to object or one of the interfaces they
implement. They get unboxed when cast back to the value type. Because
boxes are objects that are allocated on the heap and are garbage collected,
too much boxing and unboxing can have a negative impact on the heap,
the garbage collector, and ultimately the performance of the application.
Reference type assignments copy the reference, whereas value type
assignments copy the entire value. Therefore assignments of large reference
types are cheaper than assignments of large value types.
Finally, reference types are passed by reference, whereas value types are
passed by value. Changes to an instance of a reference type affect all references
pointing to the instance. Value type instances are copied when they
are passed by value. When an instance of a value type is changed, it of
course does not affect any of its copies. Because the copies are not created
explicitly by the user, but rather implicitly when arguments are passed or
return values are returned, value types that can be changed can be confusing
to many users. Therefore value types should be immutable. (1)
|
As a rule of thumb, majority of types in a framework should be classes.
There are, however, some situations in which the characteristics of a value
type make it more appropriate to use structs.
CONSIDER defining a struct instead of a class if instances of the type are
small and commonly short-lived or are commonly embedded in other
objects.
DO NOT define a struct unless the type has all of the following characteristics:
- It logically represents a single value, similar to primitive types (int,
double, etc.). - It has an instance size under 16 bytes.
- It is immutable.
- It will not have to be boxed frequently.
In all other cases, you should define your types as classes.
|
Choosing Between Class and Interface
In general, classes are the preferred construct for exposing abstractions.
The main drawback of interfaces is that they are much less flexible than
classes when it comes to allowing for evolution of APIs. Once you ship an
interface, the set of its members is fixed forever. Any additions to the interface
would break existing types implementing the interface.
A class offers much more flexibility. You can add members to classes
that have already shipped. As long as the method is not abstract (i.e., as
long as you provide a default implementation of the method), any existing
derived classes continue to function unchanged.
Let’s illustrate the concept with a real example from the .NET Framework.
The System.IO.Stream abstract class shipped in version 1.0 of the
framework without any support for timing out pending I/O operations. In
version 2.0, several members were added to Stream to allow subclasses to
support timeout-related operations, even when accessed through their
base class APIs.
public abstract class Stream { public virtual bool CanTimeout { get { return false; } } public virtual int ReadTimeout{ get{ throw new NotSupportedException(…); { set { throw new NotSupportedException(…); } } } public class FileStream : Stream { public override bool CanTimeout { get { return true; } } public override int ReadTimeout{ get{ … { set { … } } }
The only way to evolve interface-based APIs is to add a new interface
with the additional members. This might seem like a good option, but it
suffers from several problems. Let’s illustrate this on a hypothetical
IStream interface. Let’s assume we had shipped the following APIs in version
1.0 of the Framework.
public interface IStream { … } public class FileStream : IStream { … }
If we wanted to add support for timeouts to streams in version 2.0, we
would have to do something like the following:
public interface ITimeoutEnabledStream : IStream { int ReadTimeout{ get; set; } } public class FileStream : ITimeoutEnabledStream { public int ReadTimeout{ get{ … { set { … } } }
But now we would have a problem with all the existing APIs that consume
and return IStream. For example StreamReader has several constructor
overloads and a property typed as Stream.
public calss StreamReader { public StreamReader(IStream stream){ … } public IStream BaseStream { get { … } } }
How would we add support for ITimeoutEnabledStream to Stream-
Reader? We would have several options, each with substantial development
cost and usability issues:
Leave the StreamReader as is, and ask users who want to access the
timeout-related APIs on the instance returned from BaseStream property
to use a dynamic cast and query for the ITimeoutEnabledStream interface.
StreamReader reader = GetSomeReader(); ITimeoutEnabledStream stream = reader.BaseStream as ITimeoutEnabledStream; if(stream != null){ stream.ReadTimeout = 100; }
This option unfortunately does not perform well in usability studies. The
fact that some streams can now support the new operations is not immediately
visible to the users of StreamReader APIs. Also, some developers
have difficulties understanding and using dynamic casts.
Add a new property to StreamReader that would return ITimeout-
EnabledStream if one was passed to the constructor or null if IStream
was passed.
StreamReader reader = GetSomeReader(); ITimeoutEnabledStream stream = reader.TimeoutEnabledBaseStream; if(stream!= null){ stream.ReadTimeout = 100; }
Such APIs are only marginally better in terms of usability. It’s really not
obvious to the user that the TimeoutEnabledBaseStream property getter
might return null, which results in confusing and often unexpected Null-
ReferenceExceptions.
1. Immutable types are types that don’t have any public members that can modify this
instance. For example, System.String is immutable. Its members, such as ToUpper,
do not modify the sting on which they are called, but rather return a new modified
string and leave the original string unchanged.
Add a new type called TimeoutEnabledStreamReader that would
take ITimeoutEnabledStream parameters to the constructor overloads
and return ITimeoutEnabledStream from the BaseStream property. The
problem with this approach is that every additional type in the framework
adds complexity for the users. What’s worse, the solution usually creates
more problems like the one it is trying to solve. StreamReader itself is
used in other APIs. These other APIs will now need new versions that can
operate on the new TimeoutEnabledStreamReader.
The Framework streaming APIs are based on an abstract class. This
allowed for an addition of timeout functionality in version 2.0 of the
Framework. The addition is straightforward, discoverable, and had little
impact on other parts of the framework.
StreamReader reader = GetSomeReader(); if(reader.BaseStream.CanTimeout){ reader.BaseStream.ReadTimeout = 100; }
One of the most common arguments in favor of interfaces is that they
allow separating contract from the implementation. However, the argument
incorrectly assumes that you cannot separate contracts from implementation
using classes. Abstract classes residing in a separate assembly
from their concrete implementations are a great way to achieve such separation.
For example, the contract of IList
added to a collection, the Count property is incremented by one. Such a
simple contract can be expressed and, what’s more important, locked for
all subtypes, using the following abstract class:
public abstract class CollectionContract: IList { public void Add(T item){ AddCore(item); this.count++; } public int Count { get { return this.count; } } protected abstract void AddCore(T item); private int count; }
|
COM exposed APIs exclusively through interfaces, but you should not
assume that COM did this because interfaces were superior. COM did it
because COM is an interface standard that was intended to be supported
on many execution environments. CLR is an execution standard and it provides
a great benefit for libraries that rely on portable implementation.
DO favor defining classes over interfaces.
Class-based APIs can be evolved with much greater ease than interfacebased
APIs because it is possible to add members to a class without
breaking existing code.
|
|
DO use abstract classes instead of interfaces to decouple the contract
from implementations.
Abstract classes, if designed correctly, allow for the same degree of
decoupling between contract and implementation.
|
|
DO define an interface if you need to provide a polymorphic hierarchy
of value types.
Value types cannot inherit from other types, but they can implement
interfaces. For example, IComparable, IFormattable, and IConvertible
are all interfaces so value types such as Int32, Int64, and other
primitives can all be comparable, formattable, and convertible.
public struct Int32 : IComparable, IFormattable, IConvertible { … } public struct Int64 : IComparable, IFormattable, IConvertible { … }
CONSIDER defining interfaces to achieve a similar effect to that of multiple
inheritance.
|
|
For example, System.IDisposable and System.ICloneable are
both interfaces so types, like System.Drawing.Image, can be both disposable,
cloneable, and still inherit from System.MarshalByRef-
Object class.
public class Image : MarshalByRefObject, IDisposable, ICloneable { … }
|
Abstract Class Design
DO NOT define public or protected-internal constructors in abstract
types.
Constructors should be public only if users will need to create instances
of the type. Because you cannot create instances of an abstract type, an
abstract type with a public constructor is incorrectly designed and misleading
to the users.(2)
// bad design public abstract class Claim { public Claim (int number) { } } // good design public abstract class Claim { // incorrect Design protected Claim (int number) { } }
DO define a protected or an internal constructor on abstract classes.
A protected constructor is more common and simply allows the base
class to do its own initialization when subtypes are created.
public abstract class Claim { protected Claim() { … } }
An internal constructor can be used to limit concrete implementations
of the abstract class to the assembly defining the class.
public abstract class Claim { internal Claim() { … } }
|
DO provide at least one concrete type that inherits from each abstract
class that you ship.
This helps to validate the design of the abstract class. For example, System.
IO.FileStream is an implementation of the System.IO.Stream
abstract class.
|
Static Class Design
A static class is defined as a class that contains only static members (of
course besides the instance members inherited from System.Object and
possibly a private constructor). Some languages provide built-in support
for static classes. In C# 2.0, when a class is declared to be static, it is sealed,
abstract, and no instance members can be overridden or declared.
public static class File { … }
If your language does not have built-in support for static classes, you
can declare such classes manually as in the following C++ example:
public class File abstract sealed { … }
Static classes are a compromise between pure object-oriented design and
simplicity. They are commonly used to provide shortcuts to other operations
(such as System.IO.File), or functionality for which a full objectoriented
wrapper is unwarranted (such as System.Environment).
DO use static classes sparingly.
Static classes should be used only as supporting classes for the objectoriented
core of the framework.
DO NOT treat static classes as a miscellaneous bucket.
There should be a clear charter for the class.
DO NOT declare or override instance members in static classes.
DO declare static classes as sealed, abstract, and add a private instance
constructor, if your programming language does not have built-in support
for static classes.
If I were inventing a new language, I would explicitly add the concept of |
|
2. This also applies to protected-internal constructors.
Interface Design
Although most APIs are best modeled using classes and structs, there are
cases in which interfaces are more appropriate or are the only option.
The CLR does not support multiple inheritance (i.e., CLR classes cannot
inherit from more than one base class), but it does allow types to implement
one or more interfaces in addition to inheriting from a base class.
Therefore interfaces are often used to achieve the effect of multiple inheritance.
For example, IDisposable is an interface that allows types to support disposability independent of any other inheritance hierarchy in which they want to participate.
public class Component : MarshalByRefObject, IDisposable, IComponent { … }
The other situation in which defining an interface is appropriate is in
creating a common interface that can be supported by several types including
some value types. Value types cannot inherit from types other than
System.ValueType, but they can implement interfaces, so using an interface
is the only option to provide a common base type.
public struct Boolean : IComparable { … } public class String: IComparable { … }
DO define an interface if you need some common API to be supported
by a set of types that includes value types.
CONSIDER defining an interface if you need to support its functionality
on types that already inherit from some other type.
AVOID using marker interfaces (interfaces with no members).
If you need to mark a class as having a specific characteristic (marker),
in general, use a custom attribute rather than an interface.
// Avoid public interface IImmutable {} // empty interface public class Key: IImmutable { … } //Do [Immutable] public class Key { … }
Methods can be implemented to reject parameters that are not marked
with a specific attribute as follows:
public void Add(Key key, object value){ if(!key.GetType().IsDefined(typeof(ImmutableAttribute), false)){ throw new ArgumentException("The parameter must be immutable","key"); } … }
|
The problem with this approach is that the check for the custom
attribute can occur only at runtime. Sometimes, it is very important that
the check for the marker be done at compile-time. For example, a
method that can serialize objects of any type might be more concerned
with verifying the presence of the marker than with type verification at
compile-time. Using marker interfaces might be acceptable in such situations.
The following example illustrates this design approach:
public interface ITextSerializable {} // empty interface public void Serialize(ITextSerializable item){ // use reflection to serialize all public properties … }
DO provide at least one type that is an implementation of an interface.
This helps to validate the design of the interface. For example,
System.Collections.ArrayList is an implementation of the System.Collections.IList interface.
DO provide at least one API consuming each interface you define (a
method taking the interface as a parameter or a property typed as the
interface).
This helps to validate the interface design. For example, List
consumes IComparer
DO NOT add members to an interface that has previously shipped.
Doing so would break implementations of the interface. You should
create a new interface to avoid versioning problems.
Except for the situations described in these guidelines, you should, in
general, choose classes rather than interfaces in designing managed code
reusable libraries.
Struct Design
The general-purpose value type is most often referred to as a struct, its C#
keyword. This section provides some guidelines for general struct design.
Section 4.8 presents guidelines for the design of a special case of value
type, the enum.
DO NOT provide a default constructor for a struct.
This allows arrays of structs to be created without having to run the
constructor on each item of the array. Notice that C# does not allow
structs to have default constructors.
DO ensure that a state where all instance data is set to zero, false, or null
(as appropriate) is valid.
This prevents accidental creation of invalid instances when an array of
the structs is created. For example, the following struct is incorrectly
designed. The parameterized constructor is meant to ensure valid state,
but the constructor is not executed when an array of the struct is created
and so the instance filed value gets initialized to 0, which is not a valid
value for this type.
// bad design public struct PositiveInteger { int value; public PositiveInteger(int value) { if (value <= 0) throw new ArgumentException(…); this.value = value; } public override string ToString() { return value.ToString(); } }
The problem can be fixed by ensuring that the default state (in this case
the value field equal to 0) is a valid logical state for the type.
// good design public struct PositiveInteger { int value; // the logical value is value+1 public PositiveInteger(int value) { if (value <= 0) throw new ArgumentException(…); this.value = value-1; } public override string ToString() { return (value+1).ToString(); } }
DO implement IEquatable
The Object.Equals method on value types causes boxing and its
default implementation is not very efficient, as it uses reflection.
IEquatable
be implemented such that it will not cause boxing.
DO NOT explicitly extend System.ValueType. In fact, most languages
prevent this.
In general, structs can be very useful, but should only be used for small,
single, immutable values that will not be boxed frequently. Next are guidelines
for enum design, a more complex matter.
Enum Design
Enums are a special kind of value type. There are two kinds of enums: simple
enums and flag enums.
Simple enums represent small, closed sets of choices. A common example
of the simple enum is a set of colors. For example,
public enum Color { Red, Green, Blue, … }
Flag enums are designed to support bitwise operations on the enum
values. A common example of the flags enum is a list of options. For
example,
[Flags] public enum AttributeTargets { Assembly= 0x0001, Module = 0x0002, Cass = 0x0004, Struct = 0x0008, … }
|
Where it could get worse, I think, is that if less advanced developers don't |
Historically, many reusable libraries (e.g., Win32 APIs) represented sets
of values using integer constants. Enums make such sets more strongly
typed, and thus improve compile-time error checking, usability, and readability.
For example, use of enums allows development tools to know the
possible values for a property or a parameter.
DO use an enum to strongly type parameters, properties, and return
values that represent sets of values.
DO favor using an enum over static constants.
// Avoid the following public static class Color { public static int Red = 0; public static int Green = 1; public static int Blue = 2; … } // Favor the following public enum Color { Red, Green, Blue, … }
|
DO NOT use an enum for open sets (such as the operating system version,
names of your friends, etc.).
DO NOT provide reserved enum values that are intended for future use.
You can always simply add values to the existing enum at a later stage.
See section "Adding Values to Enums" for more details on adding values to enums. Reserved
values just pollute the set of real values and tend to lead to user errors.
public enum DeskType { Circular, Oblong, Rectangular, // the following two values should not be here ReservedForFutureUse1, ReservedForFutureUse2, }
AVOID publicly exposing enums with only one value.
A common practice for ensuring future extensibility of C APIs is to add
reserved parameters to method signatures. Such reserved parameters
can be expressed as enums with a single default value. This should not
be done in managed APIs. Method overloading allows adding parameters
in future releases.
// Bad Design public enum SomeOption { DefaultOption // we will add more options in the future } … // The option parameter is not needed. // It can always be added in the future // to an overload of SomeMethod(). public void SomeMethod(SomeOption option) { … }
DO NOT include sentinel values in enums.
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 |
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 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 If no such default exists, I make zero my "error" (none-of-the-above) In either case, however, from the compiler (and runtime), point of view, |
Adding Values to Enums
It is very common to discover that you need to add values to an enum after
you have already shipped it. There is a potential application compatibility
problem when the newly added value is returned from an existing API,
because poorly written applications might not handle the new value correctly.
Documentation, samples, and FxCop rules encourage application
developers to write robust code that can help applications deal with unexpected
values. Therefore, it is generally acceptable to add values to enums,
but as with most guidelines there might be exceptions to the rule based on
the specifics of the framework.
CONSIDER adding values to enums, despite a small compatibility risk.
If you have real data about application incompatibilities caused by
additions to an enum, consider adding a new API that returns the new
and old values, and deprecate the old API, which should continue
returning just the old values. This will ensure that your existing applications
remain compatible.
The biggest concern with adding values to enums is that you don't Clients might instead perform point-wise case analyses across their |
Nested Types
A nested type is a type defined within the scope of another type, which is
called the enclosing type. A nested type has access to all members of its enclosing
type. For example, it has access to private fields defined in the
enclosing type and to protected fields defined in all ascendants of the enclosing
type.
// enclosing type public class OuterType { private string name; // nested type public class InnerType { public InnerType(OuterType outer){ // the name field is private, but it works just fine Console.WriteLine(outer.name); } } }
In general, nested types should be used sparingly. There are several reasons
for this. Some developers are not fully familiar with the concept. These developers might, for example, have problems with the syntax of declaring variables of nested types. Nested types are also very tightly coupled with their enclosing types, and as such are not suited to be generalpurpose
types.
Nested types are best suited for modeling implementation details of
their enclosing types. The end user should rarely have to declare variables
of a nested type and almost never explicitly instantiate nested types. For example, the enumerator of a collection can be a nested type of that collection. Enumerators are usually instantiated by their enclosing type and because many languages support the foreach statement, enumerator variables
rarely have to be declared by the end user.
DO use nested types when the relationship between the nested type
and its outer type is such that member-accessibility semantics are
desirable.
For example, the nested type needs to have access to private members
of the outer-type.
public OrderCollection : IEnumerable{ Order[] data = …; public IEnumerator GetEnumerator(){ return new OrderEnumerator(this); } // This nested type will have access to the data array // of its outer type. class OrderEnumerator : IEnumerator { } }
DO NOT use public nested types as a logical grouping construct; use
namespaces for this.
AVOID publicly exposed nested types. The only exception to this is if
variables of the nested type need to be declared only in rare scenarios
such as subclassing or other advanced customization scenarios.
|
DO NOT use nested types if the type is likely to be referenced outside of
the containing type.
For example, an enum passed to a method defined on a class should not
be defined as a nested type in the class.
DO NOT use nested types if they need to be instantiated by client code. If
a type has a public constructor, it should probably not be nested.
If a type can be instantiated, it seems to indicate that the type has a
place in the framework on its own (you can create it, work with it, and
destroy it, without ever using the outer type), and thus should not be
nested. Inner types should not be widely reused outside of the outer
type without any relationship whatsoever to the outer type.
DO NOT define a nested type as a member of an interface. Many languages
do not support such a construct.
In general, nested types should be used sparingly, and exposure as public
types should be avoided.
Summary
This article presented guidelines that describe when and how to design
classes, structs, and interfaces.
About the Authors
Krzysztof Cwalina is a Program Manager on the Common Language Runtime team at Microsoft Corporation. He began his career at Microsoft designing APIs for the first release of the .NET Framework. He has been responsible for several namespaces in the Framework, including System.Collections, System.Diagnostics, System.Messaging, and others. He was also one of the original members of the FxCop team. Currently, he is leading a companywide effort to develop, promote, and apply the design guidelines to the .NET Framework and WinFX. Krzysztof graduated with a B.S. and an M.S. in computer science from the University of Iowa.
Brad Abrams is a Program Manager on the .NET Framework team at Microsoft, where he has been designing the Framework Class Libraries for the past five years. He is the primary author of the .NET Framework Design Guidelines, the Common Language Specification, and the class libraries for the ECMA CLI specification.
About the Book
Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries
By Krzysztof Cwalina and Brad Adams
Published: September 19, 2005, Paperback: 384 pages
Published by Addison-Wesley
ISBN: 0321246756
Retail price: $39.99
This material is from Chapter 4 of the book.