4

Type Design Guidelines

From the CLR perspective, there are only two categories of types— reference types and value types—but for the purpose of discussing framework design, we divide types into more logical groups, each with its own specific design rules. Figure 4-1 shows these logical groups.

Images

Figure 4-1: The logical grouping of types

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. Extensibility and base classes are covered in Chapter 6.

Interfaces are types that can be implemented by both reference types and value types. As a consequence, they 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. Guidelines for their design and usage are discussed elsewhere in this book.

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

4.1 Types and Namespaces

You should decide how to factor your functionality into a set of functional areas represented by namespaces when you design a large framework. This kind of top-down architectural design is important because it ensures a coherent set of namespaces containing types that work well together. The namespace design process is iterative, of course, 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 philosophy leads to the following guidelines.

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

Images AVOID very deep namespace hierarchies. Such hierarchies are difficult to browse because the user has to backtrack often.

Images AVOID having too many namespaces.

Users of a framework should not have to import many namespaces in the most common scenarios. Types that are used together in common scenarios should reside in a single namespace if at all possible. This guideline does not mean “have only one namespace,” but instead urges you to seek balance. The types used by developers and the types used by code generators or IDE designers to help the developers are two different concepts and should be in different, but related, namespaces.

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

Images DO NOT define types without specifying their namespaces.

This practice organizes related types in a hierarchy and can help resolve potential type name collisions. Of course, the fact that namespaces can help resolve name collisions does not mean that such collisions should be introduced. See section 3.4.1 for details.

4.2 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 generally 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 they are cast to a reference type or one of the interfaces they implement. They get unboxed when they are 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 when 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 default to being passed by value. Changes to an instance of a reference type affect all references pointing to that instance. Value type instances are copied when they are passed by value. When an instance of a value type is changed, it does not affect any of its copies. Because the copies are not created explicitly by the user but rather are created 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

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 string on which they are called, but rather return a new modified string instead and leave the original string unchanged.

The downside to this technique is that you don’t get garbage collection on elements in your array. Thus, this technique should be used only when that is not an issue (e.g., when you have a large read-only array of small structures).

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.

Images 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, especially arrays.

Images 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 less than 24 bytes.

  • It is immutable.

  • It will not have to be boxed frequently.

In all other cases, you should define your types as classes.

4.3 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. After you ship an interface, the set of its members is largely fixed forever. Any additions to the interface would break existing types that implement 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 .NET. The System.IO.Stream abstract class shipped in .NET Framework 1.0 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.

Click here to view code image

public abstract class Stream {
    public virtual bool CanTimeout {
      get { return false; }
    }
    public virtual int ReadTimeout{
      get {
           throw new InvalidOperationException(...);
      }
     set {
           throw new InvalidOperationException(...);
     }
   }
}
public class FileStream : Stream {
   public override bool CanTimeout {
      get { return true; }
   }
   public override int ReadTimeout{
      get {
        ...
      }
      set {
        ...
      }
    }
}

The only general way to evolve interface-based APIs without runtime or compile-time errors 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 with a hypothetical IStream interface. Let’s assume we had shipped the following APIs in .NET Framework 1.0.

Click here to view code image

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:

Click here to view code image

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.

Click here to view code image

public class StreamReader {
  public StreamReader(IStream stream){ ... }
  public IStream BaseStream { get { ... } }
}

How would we add support for ITimeoutEnabledStream to StreamReader? 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 ITimeoutEnabled Stream interface.

    Click here to view code image

    StreamReader reader = GetSomeReader();
    var stream = reader.BaseStream as ITimeoutEnabledStream;
    if(stream != null){
        stream.ReadTimeout = 1ʘʘ;
    }

    Unfortunately, this option does not perform well in usability studies. The fact that some streams can now support the new operations is not immediately apparent to the users of StreamReader APIs. Also, some developers have difficulty understanding and using dynamic casts.

  • Add a new property to StreamReader that would return ITimeoutEnabledStream if one is passed to the constructor or null if IStream is passed.

    Click here to view code image

    StreamReader reader = GetSomeReader();
    var stream = reader.TimeoutEnabledBaseStream;
    if(stream != null){
        stream.ReadTimeout = 1ʘʘ;
    }

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

  • 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 .NET 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, is discoverable, and had little impact on other parts of the framework.

    Click here to view code image

    StreamReader reader = GetSomeReader();
    if(reader.BaseStream.CanTimeout){
        reader.BaseStream.ReadTimeout = 1ʘʘ;
    }

One of the most common arguments in favor of interfaces is that they allow for separating the contract from the implementation. However, this 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<T> says that when an item is added to a collection, the Count property is incremented by one. Such a simple contract can be expressed—and, more importantly, locked— for all subtypes, using the following abstract class:

Click here to view code image

public abstract class CollectionContract<T> : IList<T> {

  public void Add(T item){
    AddCore(item);
    _count++;
  }
  public int Count {
    get { return _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.

Images DO favor defining classes over interfaces.

Class-based APIs can be evolved with much greater ease than interface-based APIs because it is possible to add members to a class without breaking existing code.

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

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

Click here to view code image

public struct Int32 : IComparable, IFormattable, IConvertible {
  ...
}
public struct Int64 : IComparable, IFormattable, IConvertible {
  ...
}

Images 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 and cloneable yet still inherit from the System.MarshalByRefObject class.

Click here to view code image

public class Image : MarshalByRefObject, IDisposable, ICloneable {
   ...
}

4.4 Abstract Class Design

Images 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

2. This also applies to protected internal constructors.

Click here to view code image

// bad design
public abstract class Claim {
  public Claim() {
  }
}
// good design
public abstract class Claim {
  protected Claim() {
  }
}

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

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() {
    ...
  }
}

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

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

public static class File {
    ...
}

If your language does not have built-in support for static classes, you can declare such classes manually, as shown in the following C++ example:

Click here to view code image

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), holders of extension methods, or functionality for which a full object-oriented wrapper is unwarranted (such as System.Environment).

Images DO use static classes sparingly.

Static classes should be used only as supporting classes for the object-oriented core of the framework.

Images DO NOT treat static classes as a miscellaneous bucket.

There should be a clear charter for each class. When your description of the class involves “and” or a new sentence, you need another class.

Images DO NOT declare or override instance members in static classes.

This is enforced by the C# compiler.

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

4.6 Interface Design

Although most APIs are best modeled using classes and structs, there are some 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.

Click here to view code image

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.

Click here to view code image

public struct Boolean : IComparable {
  ...
}
public class String: IComparable {
  ...
}

Images DO define an interface if you need some common API to be supported by a set of types that includes value types.

Images CONSIDER defining an interface if you need to support its functionality on types that already inherit from some other type.

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

Click here to view code image

// Avoid
public interface IImmutable {} // empty interface
public class Key: IImmutable {
   ...
}
// Consider
[Immutable]
public class Key {
   ...
}

Methods can be implemented to reject parameters that are not marked with a specific attribute, as follows:

Click here to view code image

public void Add(Key key, object value){
  if(!key.GetType().IsDefined(typeof(ImmutableAttribute), false)){
     throw new ArgumentException("The argument must be declared
[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:

Click here to view code image

public interface ITextSerializable {} // empty interface
public void Serialize(ITextSerializable item){
  // use reflection to serialize all public properties
   ...
}

Images 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, System.Collections.Generic.List<T> is an implementation of the System.Collections.Generic.IList<T> interface.

Images 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 IComparer<T> interface.

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

4.7 Struct Design

The general-purpose value type is most often referred to as a struct, its C# keyword. This section provides guidelines for general struct design. Section 4.8 presents guidelines for the design of a special case of value type, the enum.

Images DO NOT provide a default constructor for a struct.

Many CLR languages do not allow developers to define default constructors on value types. Users of these languages are often surprised to learn that default(SomeStruct) and new SomeStruct() don’t necessarily produce the same value. Even if your language does allow defining a default constructor on a value type, it probably isn’t worth the confusion it would cause if you did it.

Images 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, 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.

Click here to view code image

// bad design
public struct ZipCode {
   public int FiveDigitCode { get; set; } // get/set properties
   public int PlusFourExtension { get; set; }
}

// good design
public struct ZipCode {
   public ZipCode(int fiveDigitCode, int plusFourExtension){...}
   public ZipCode(int fiveDigitCode):this(fiveDigitCode,ʘ){}

   public int FiveDigitCode { get; } // get-only properties
   public int PlusFourExtension { get; }
}

Images DO declare immutable value types with the readonly modifier.

Newer compiler understand the readonly modifier on a value type and avoid making extra value copies on operations such as invoking a method on a field declared with the readonly modifier.

Click here to view code image

public readonly struct ZipCode {
   public ZipCode(int fiveDigitCode, int plusFourExtension){...}
   public ZipCode(int fiveDigitCode):this(fiveDigitCode,ʘ){}

   public int FiveDigitCode { get; } // get-only properties
   public int PlusFourExtension { get; }

   public override string ToString() {
      ...
   }
}

public partial class Other {
   private readonly ZipCode _zipCode;

   ...
   private void Work() {
      // Because ZipCode is declared as "readonly struct" the
      // ToString() method call does not involve a defensive copy
      string zip = _zipCode.ToString();
      ...
   }
}

Images DO declare nonmutating methods on mutable value types with the readonly modifier.

For better or worse, there are public mutable value types in .NET. As mentioned in the guidance against having mutable value types, when a mutable value type is stored in a field declared with the readonly modifier, any method or property invocation is performed on a copy of the value. Like the readonly modifier at the type level, the readonly modifier on a method allows the compiler to skip copying the value before invoking the method.

When the type is declared with the readonly modifier, there is no additional benefit from specifying the modifier on each method.

The C# compiler automatically applies the readonly modifier to the get method of a property declared using the auto-implemented property syntax.

Click here to view code image

// Using the "bad design" mutable version of ZipCode
public struct ZipCode {
   private int _plusFour;

   // This get method is implicitly readonly
   public int FiveDigitCode { get; set; }

   // This one needs it explicitly
   public int PlusFourExtension {
      readonly get => _plusFour;

      set {
         if (value > 9999) {
            value = -1;
         }
         _plusFour = value;
      }
   }

   // Any methods also need the readonly modifier
   // (if they don't mutate)
   public override readonly string ToString() { ... }
}

Images 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 a valid state, but the constructor is not executed when an array of the struct is created. Thus, the instance field value gets initialized to 0, which is not a valid value for this type.

Click here to view code image

// bad design
public struct PositiveInteger {
  private int value;

  public PositiveInteger(int value) {
    if (value <= ʘ) throw new ArgumentException(...);
    _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.

Click here to view code image

// good design
public struct PositiveInteger {
  private int _value; // the logical value is value+1

  public PositiveInteger(int value) {
    if (value <= ʘ) throw new ArgumentException(...);
    _value = value-1;
  }

  public override string ToString() {
    return (_value+1).ToString();
  }
}

Images DO NOT define ref-like value types (ref struct types), other than for specialized low-level purposes where performance is critical.

A ref struct type is an advanced concept that comes with several restrictions. Values from a ref struct type are allowed to exist only on the stack, and can never be boxed into the heap. Consequently, a ref struct type cannot be used as the type for a field in another type, except for other ref struct types, and cannot be used in asynchronous methods generated with the async keyword.

These usability limitations are a source of confusion and frustration for less advanced developers, and should generally be avoided.

Images 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. IEquatable<T>.Equals can have much better performance and can be implemented so that it will not cause boxing. See section 8.6 for guidelines on implementing IEquatable<T>.

Images DO NOT explicitly extend System.ValueType. In fact, most languages prevent this.

In general, structs can be very useful, but they should be used only for small, single, immutable values that will not be boxed frequently.

4.8 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 such as the following:

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 like the following:

Click here to view code image

[Flags]
public enum AttributeTargets {
   Assembly = ʘxʘʘʘ1,
   Module   = ʘxʘʘʘ2,
   Cass     = ʘxʘʘʘ4,
   Struct   = ʘxʘʘʘ8,
   ...
}

Historically, many APIs (e.g., Win32 APIs) represented sets of values using integer constants. Enums make such sets more strongly typed, thereby improving compile-time error checking, usability, and readability. For example, use of enums allows development tools to know the possible choices for a property or a parameter.

Images DO use an enum to strongly type parameters, properties, and return values that represent sets of values.

Images DO favor using an enum instead of static constants.

IntelliSense provides support for specifying arguments to members with enum parameters. It does not have similar support for static constants.

Click here to view code image

// Avoid the following
public static class Color {
   public static int Red    = ʘ;
   public static int Green  = 1;
   public static int Blue   = 2;
   ...
}

// Favor the following
public enum Color {
   Red,
   Green,
   Blue,
   ...
}

Images DO NOT use an enum for open sets (such as the operating system version, names of your friends, etc.).

Images 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. Section 4.8.2 provides more details on adding values to enums. Reserved values just pollute the set of real values and tend to lead to user errors.

Click here to view code image

public enum DeskType {
  Circular,
  Oblong,
  Rectangular,

  // the following two values should not be here
  ReservedForFutureUse1,
  ReservedForFutureUse2,
}

Images 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 practice should not be followed in managed APIs. Method overloading allows adding parameters in future releases.

Click here to view code image

// 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) {
  ...
}

Images 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. The following example shows an enum with an additional sentinel value used to identify the last value of the enum, which is intended for use in range checks. This is bad practice in framework design.

Click here to view code image

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.

Click here to view code image

public void OrderDesk(DeskType desk){
  if(desk > DeskType.Rectangular || desk < DeskType.Circular){
    throw new ArgumentOutOfRangeException(...);
  }
  ...
}

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

public enum Compression {
  None = ʘ,
  GZip,
  Deflate,
}
public enum EventType {
  Error = ʘ,
  Warning,
  Information,
  ...
}

Images 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 will always be rounded up to a DWORD.

Images DO name flag enums with plural nouns or noun phrases and simple enums with singular nouns or noun phrases.

See section 3.5.3 for details.

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

Simple enums works well for a closed set of alternatives. When multiple values need to be expressed concurrently, consider a flag enum (section 4.8.1). When the set of alternatives should be open to allow for derived types to add new capabilities, consider the strongly typed string approach discussed in section 4.11.

4.8.1 Designing Flag Enums

Images DO apply the System.FlagsAttribute to flag enums. Do not apply this attribute to simple enums.

Click here to view code image

[Flags]
public enum AttributeTargets {
   ...
}

Images DO use powers of 2 for the flag enum values so they can be freely combined using the bitwise OR operation.

Click here to view code image

[Flags]
public enum WatcherChangeTypes {
  None = ʘ,
  Created = ʘxʘʘʘ2,
  Deleted = ʘxʘʘʘ4,
  Changed = ʘxʘʘʘ8,
  Renamed = ʘxʘʘ1ʘ,
}

Images 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
}

Images 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 {
   Default = ʘ,

   Instance = ʘx4,
   Static = ʘx8,

   Public = ʘx1ʘ,
   NonPublic = ʘx2ʘ,

   CreateInstance = ʘx2ʘʘ,
   GetField = ʘx4ʘʘ,
   SetField = ʘx8ʘʘ,
   GetProperty = ʘx1ʘʘʘ,
   SetProperty = ʘx2ʘʘʘ,
   InvokeMethod = ʘx1ʘʘ,
   ...
}

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:

Click here to view code image

[Flags]
public enum Visibilities {
   None = ʘ,
   Public = ʘx1ʘ,
   NonPublic = ʘx2ʘ,
}

[Flags]
public enum MemberScopes {
   None = ʘ,
   Instance = ʘx4,
   Static = ʘx8,
}

[Flags]
public enum MemberKinds {
   None = ʘ,
   Constructor = 1 << ʘ,
   Field = 1 << 1,
   PropertyGetter = 1 << 2,
   PropertySetter = 1 << 3,
   Method = 1 << 4,
}

public class Type {
   public MemberInfo[] GetMembers(MemberKinds members,
                                  Visibilities visibility,
                                  MemberScopes scope);
}

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

The following example shows a common implementation of a check that programmers use to determine if a flag is set (see the if-statement in the example). The check works as expected for all flag enum values except the value of zero, where the Boolean expression always evaluates to true.

Click here to view code image

[Flags]
public enum SomeFlag {
  ValueA = ʘ,  // this might be confusing to users
  ValueB = 1,
  ValueC = 2,
  ValueBAndC = ValueB | ValueC,
}

SomeFlag flags = GetValue();
if ((flags & SomeFlag.ValueA) == SomeFlag.ValueA) {
    ...
}

Images DO name the zero value of flag enums None. For a flag enum, the value must always mean “all flags are cleared.”

Click here to view code image

[Flags]
public enum BorderStyle {
  Fixed3D   = ʘx1,
  FixedSingle  = ʘx2,
  None  = ʘxʘ
}
if (foo.BorderStyle == BorderStyle.None)....

4.8.2 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. A potential application compatibility problem arises 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 code analysis tools 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.

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

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

Click here to view code image

// 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, for several reasons. 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 should almost never 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.

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

Click here to view code image

public OrderCollection : IEnumerable<Order> {
  private Order[] _data = ...;

  public IEnumerator<Order> GetEnumerator(){
    return new OrderEnumerator(this);
  }

  // This nested type will have access to the data array
  // of its outer type.
  private class OrderEnumerator : IEnumerator<Order> {
  }
}

Images DO NOT use public nested types as a logical grouping construct; use namespaces for this purpose.

Images AVOID publicly exposed nested types. The only exception to this guideline is if variables of the nested type need to be declared only in rare scenarios such as subclassing or other advanced customization scenarios.

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

Images DO NOT use nested types if they need to be instantiated by client code.

If a type has a public constructor, it probably should 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.

Images DO NOT define a nested type as a member of an interface. Many languages do not support such a construct.

In general, use nested types sparingly, and avoid their exposure as public types.

4.10 Types and Assembly Metadata

Types reside in assemblies, which in most cases are packaged in the form of DLLs or executables (EXEs). Several important attributes should be applied to assemblies that contain public types. This section describes guidelines related to these attributes.

Images DO apply the CLSCompliant(true) attribute to assemblies with public types.

[assembly:CLSCompliant(true)]

The attribute is a declaration that the types contained in the assembly are CLS3 compliant and so can be used by all .NET languages.4 Some languages, such as C#, verify compliance5 with the standard if the attribute is applied.

3. The CLS standard is an interoperability agreement between framework developers and language developers as to what subset of the CLR type system can be used in framework APIs so that these APIs can be used by all CLS-compliant languages.

4. All that support the CLS standard—and almost all of the CLR languages do.

5. C# verifies compliance with most of the CLS rules. Some rules are not automatically verifiable. For example, the standard does not say that an assembly cannot have non-compliant APIs. Instead, it says only that noncompliant APIs must be marked with CLSCompliant(false) and that a compliant alternative must exist. But, of course, the existence of an alternative to a noncompliant API cannot be verified automatically.

For example, unsigned integers are not in the CLS subset. Therefore, if you add an API that uses UInt32 (for example), C# will generate a compilation warning. To comply with the CLS standard, the assembly must provide a compliant alternative and explicitly declare the noncompliant API as CLSCompliant(false).

Click here to view code image

public static class Console {

  [CLSCompliant(false)]
  public void Write(uint value); // not CLS compliant

  public void Write(long value); // CLS-compliant alternative
}

Images DO apply AssemblyVersionAttribute to assemblies with public types.

Click here to view code image

[assembly:AssemblyVersion(...)]

Images DO apply the following informational attributes to assemblies. These attributes are used by tools, such as Visual Studio, to inform the user of the assembly about its contents.

Click here to view code image

[assembly:AssemblyTitle("System.Core.dll")]
[assembly:AssemblyCompany("Microsoft Corporation")]
[assembly:AssemblyProduct("Microsoft .NET Framework")]
[assembly:AssemblyDescription(...)]

Images CONSIDER applying ComVisible(false) to your assembly. COM-callable APIs need to be designed explicitly. As a rule of thumb, .NET assemblies should not be visible to COM. If you do design the APIs to be COM-callable, you can apply ComVisible(true) either to the individual APIs or to the whole assembly.

Images CONSIDER applying AssemblyFileVersionAttribute and Assembly-CopyrightAttribute to provide additional information about the assembly.

Images CONSIDER using the format <V>.<S>.<B>.<R> for the assembly file version, where V is the major version number, S is the servicing number, B is the build number, and R is the build revision number.

For example, this is how the version attribute is applied in System.Core.dll, which shipped as part of .NET Framework 3.5:

Click here to view code image

[assembly:AssemblyFileVersion("3.5.21022.8")]

4.11 Strongly Typed Strings

Enums generally provide compile-time assurance that an input option is valid.6 The IntelliSense-enhanced feature discovery that enums enable comes at a cost of extensibility: Derived types have no way of supporting additional options using the API provided by the base class.

6. A caller can sometimes specify a disallowed option, or end up in a situation where a library was not aware of a new enum value. But usually if an enum compiles, it’s a valid value.

The most extensible input types are Object and String, but both types suffer from a difficulty in callers’ understanding of legal input values.

Click here to view code image

public partial class RSACryptoServiceProvider {
  // What are the legal input values for "halg"?
  //
  // It turns out, it's some string values, some Type values,
  // and some HashAlgorithm values ... but most string values,
  // most Type values, and about half of the built-in
  // HashAlgorithm types' values are invalid.
  public byte[] SignData(byte[] buffer, object halg) { ... }
}

Between the flexible (but hard for callers to reason about) string input type, and the inflexible (but easy for callers to reason about) enum-based input type is the “strongly typed string.” A strongly typed string is a string, wrapped in a value type, with static properties providing the most common values.

Click here to view code image

namespace System.Security.Cryptography {
 public readonly struct HashAlgorithmName :
   IEquatable<HashAlgorithmName> {

   public static HashAlgorithmName MD5 { get; } =
     new HashAlgorithmName("MD5");

   public static HashAlgorithmName SHA1 { get; } = ...;
   public static HashAlgorithmName SHA256 { get; } = ...;
   public static HashAlgorithmName SHA384 { get; } = ...;
   public static HashAlgorithmName SHA512 { get; } = ...;

   public HashAlgorithmName(string name) {
     // Note: No validation because we have to deal with
     // default(HashAlgorithmName) regardless.
     Name = name;
   }

   public string Name { get; }
   public override string ToString() {
     return Name ?? String.Empty;
   }

   public override bool Equals(object obj) {
     return obj is HashAlgorithmName &&
       Equals((HashAlgorithmName)obj);
   }

   public bool Equals(HashAlgorithmName other) {
     // NOTE: Intentionally case-sensitive ordinal, matches OS.
     return Name == other.Name;
   }

   public override int GetHashCode() {
     return Name == null ? ʘ : Name.GetHashCode();
   }

   public static bool operator ==(
     HashAlgorithmName left,
     HashAlgorithmName right) {
     return left.Equals(right);
   }

   public static bool operator !=(
     HashAlgorithmName left,
     HashAlgorithmName right) {
     return !(left == right);
   }
 }
}

Images CONSIDER defining a strongly typed string value type when a base class supports a fixed set of inputs but a derived type could support more.

When a strongly typed string is used only by a sealed type hierarchy, there is little to no value in supporting values other than the predefined ones, and an enum is a more consistent type choice.

A strongly typed string provides the most benefit when most code treats it as if it were an enum, such as HTTP header names or cryptographic digest algorithms. When the number of possible values is too large, such as all possible filenames, the value in a strongly typed string is reduced to just parameter consistency via type checking.

A variation of strongly typed strings is used in .NET for the JsonEncodedText type, where the type indicates that the value has already been processed by a string escaping/normalization process. This is a valuable approach, but does not represent a strongly typed string.

Images DO declare a strongly typed string as an immutable value type (struct) with a string constructor.

The strongly typed string type should follow other guidance for immutable value types, such as using the readonly modifier on the type and implementing IEquatable<T>.

Images DO override ToString() on a strongly typed string to return the underlying string value.

Images CONSIDER exposing the underlying string value from a strongly typed string in a get-only property.

ToString() should be overridden because the underlying string value provides an obvious “interesting human-readable string” to return, per the guidelines on overriding ToString().

Since ToString() is used for debugging and display purposes, rather than runtime decisions, exposing the underlying string value from the property presents a better general pattern to callers when the value is required—such as interoperating with libraries that use the same string values without using the wrapping type.

If callers will only rarely, or never, need the underlying string value, then not having the property allows the type to look more like an enum to IntelliSense.

There is not a prescriptive guideline for the name of the property. If no obvious name presents itself, remember that “Value” is preferred to “String” (section 3.2.3).

For more information on overloading ToString(), see section 8.9.3.

Images DO override equality operators for strongly typed string types.

Overriding the equality operators allows for a strongly typed string type to syntactically look like either a string or an enum in common code patterns.

Strongly typed strings should use ordinal string equality by default. However, when a specific domain represents case-insensitive content, the IEquatable<T> behavior can be based on other string comparisons.

Images DO allow null inputs to the constructor of a strongly typed string.

Because value types can always be zero-initialized, the constructor of a strongly typed string should not disallow null inputs. This ensures that code logically equivalent to a copy constructor will function.

Click here to view code image

// This should always succeed.
HashAlgorithmName newName = new HashAlgorithmName(cur.Name);

Images DO declare known values for a strongly typed string via static get-only properties on the type.

The enum-like IntelliSense experience is a very strong motivator for strongly typed string types.

Images AVOID creating overloads across System.String and a strongly typed string, unless the System.String overload was released in a previous version.

The existence of an overload that accepts a System.String instance instead of a strongly typed string can save a few characters at the call site for callers that have only the underlying string value available, but it forces developers new to your API to understand the difference between the two methods.

If you have recently defined a strongly typed string type to help clarify valid inputs to a method, adding an overload that accepts the strongly typed string provides value. In that case, consider marking the original System.String-based overload as [EditorBrowsable(EditorBrowsableState.Advanced)], [EditorBrowsable(EditorBrowsableState. Never)], or [Obsolete].

Summary

This chapter presented guidelines that describe when and how to design classes, structs, and interfaces. The next chapter goes to the next level in type design—the design of members.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.100.34