Type Design Guidelines¶
From the CLR perspective, there are only two categories of types—reference types and value types—but for the purpose of a discussion about framework design, we divide types into more logical groups, each with its own specific design rules.
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 by both reference types and value types. They can thus 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 to be 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 this book.
DO ensure that each type is a well-defined set of related members, not just a random collection of unrelated functionality.
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.
The first difference between reference types and value types we will consider is that 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.
Next, 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 inline, 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.
The next difference is related to memory usage. Value types get boxed when cast to a reference type 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. In contrast, no such boxing occurs as reference types are cast.
Next, 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 are implicitly created 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.
As a rule of thumb, the 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.
AVOID defining 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.
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.
DO define a protected or an internal constructor in abstract classes.
A protected constructor is more common and simply allows the base class to do its own initialization when subtypes are created.
An internal constructor can be used to limit concrete implementations of the abstract class to the assembly defining the class.
DO provide at least one concrete type that inherits from each abstract class that you ship.
Doing 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 and later, when a class is declared to be static, it is sealed, abstract, and no instance members can be overridden or declared.
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), holders of extension methods, or functionality for which a full object-oriented wrapper is unwarranted (such as System.Environment).
DO use static classes sparingly.
Static classes should be used only as supporting classes for the object-oriented core of the framework.
DO NOT treat static classes as a miscellaneous bucket.
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.
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.
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 ValueType, but they can implement interfaces, so using an interface is the only option in order to provide a common base type.
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.
DO provide at least one type that is an implementation of an interface.
Doing this helps to validate the design of the interface. For example, List<T> is an implementation of the IList<T> interface.
DO provide at least one API that consumes each interface you define (a method taking the interface as a parameter or a property typed as the interface).
Doing this helps to validate the interface design. For example, List<T>.Sort consumes the System.Collections.Generic.IComparer<T> interface.
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 in order 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.
The general-purpose value type is most often referred to as a struct, its C# keyword. This section provides guidelines for general struct design.
DO NOT provide a default constructor for a struct.
Following this guideline 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 NOT define mutable value types.
Mutable value types have several problems. For example, when a property getter returns a value type, the caller receives a copy. Because the copy is created implicitly, developers might not be aware that they are mutating the copy, and not the original value. Also, some languages (dynamic languages, in particular) have problems using mutable value types because even local variables, when dereferenced, cause a copy to be made.
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.
DO implement IEquatable<T> on value types.
The Object.Equals method on value types causes boxing, and its default implementation is not very efficient, because it uses reflection. Equals can have much better performance and can be implemented so that it will not cause boxing.
DO NOT explicitly extend 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.
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.
Flag enums are designed to support bitwise operations on the enum values. A common example of the flags enum is a list of options.
DO use an enum to strongly type parameters, properties, and return values that represent sets of values.
DO favor using an enum instead of static constants.
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 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.
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.
DO NOT include sentinel values in enums.
Although they are sometimes helpful to framework developers, sentinel values are confusing to users of the framework. They are used to track the state of the enum rather than being one of the values from the set represented by the enum.
DO provide a value of zero on simple enums.
Consider calling the value something like “None.” If such a value is not appropriate for this particular enum, the most common default value for the enum should be assigned the underlying value of zero.
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 the 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 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 in order to make a difference, because 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.
DO use powers of two for the flag enum values so they can be freely combined using the bitwise OR operation.
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. ReadWrite is an example of such a special value.
AVOID creating flag enums where certain combinations of values are invalid.
AVOID using flag enum values of zero unless the value represents “all flags are cleared” and is named appropriately, as prescribed by the next guideline.
DO name the zero value of flag enums None. For a flag enum, the value must always mean “all flags are cleared.”
Adding Value 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.
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.
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.
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 general-purpose 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 should have to 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.
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, that seems to indicate 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.