Annex D Class Library Design Guidelines


This section contains only informative text.


This section describes the guidelines that were used in the design of the class libraries, including naming conventions and coding patterns. They are intended to give guidance to anyone who is extending the libraries, including:

  • Implementers of the CLI who wish to extend the libraries beyond those specified in this standard

  • Implementers of libraries that will run on top of the CLI and wish their libraries to be consistent with the standard libraries

  • Future standards efforts aimed at refining the existing libraries or defining additional libraries

As with any set of guidelines, they should be applied with an eye toward the end goal of consistency but understanding that for functionality, performance, or external compatibility reasons they may require modification or simply prove inappropriate in particular cases. The guidelines should not be applied blindly, and they should be revisited periodically to ensure that they remain viable.

Throughout this chapter, we use the following conventions:

  • Do means that the described practice should be followed where possible.

  • Do not means that the described practice should be avoided where possible.

  • Consider means that the described practice is often helpful, but there are common cases where it is impractical or inadvisable; thus, the practice should be carefully considered but may not be appropriate.

ANNOTATION

Although the ECMA technical committee responsible for this standard started with the Microsoft rules for designing class frameworks, the committee carefully selected a subset for the standard. The selected guidelines did not reflect any internal Microsoft ways of doing things and were generally good ideas. So the committee intends to follow these guidelines when it adds more to the standard, although it is likely to update these guidelines as well.


D.1 Naming Guidelines

One of the most important elements of predictability and discoverability in a managed class library is the use of a consistent naming pattern. Many of the most common user questions should not arise once these conventions are understood and widely used.

There are three elements of naming guidelines.

  • Case: Use the correct capitalization style.

  • Mechanics: Use nouns for classes, verbs for methods, etc.

  • Word Choice: Use terms consistently across libraries.

The following section describes rules for case and mechanics, and some philosophy regarding word choice.

D.1.1 Capitalization Styles

The following section describes different ways of capitalizing identifiers. These terms will be referred to throughout the rest of this document.

D.1.1.1 Pascal Casing

This convention capitalizes the first character of each word, as in the following example:

 
 BackColor 
D.1.1.2 Camel Casing

This convention capitalizes the first character of each word except the first word, as in the following example:.

 
 backColor 
D.1.1.3 Upper Case

Only use all uppercase letters for identifiers if it contains an abbreviation that is two characters long or less. Identifiers of three or more characters should use PascalCasing.

 
 System.IO System.Web.UI System.CodeDom 
D.1.1.4 Capitalization Summary

The following table describes the capitalization rules for different types of identifiers.

Type

Case

Notes

Class

PascalCase

 

Enum values

PascalCase

 

Enum type

PascalCase

 

Events

PascalCase

 

Exception class

PascalCase

Ends with the suffix "Exception".

Final Static field

PascalCase

 

Interface

PascalCase

Begins with the prefix "I".

Method

PascalCase

 

Namespace

PascalCase

 

Property

PascalCase

 

Public Instance Field

PascalCase

Rarely used; prefer properties.

Protected Instance Field

camelCase

Rarely used; prefer properties.

Parameter

camelCase

 

D.1.2 Word Choice
  • Do avoid using class names duplicated in heavily used namespaces. For example, do not use any of the following for a class name:

    System

    Collections

    Forms

    UI

  • Do avoid using identifiers that conflict with the following keywords:

    ANNOTATION

    To get the keywords in this table, the ECMA committee did a survey of programming languages in use and pulled out these keywords as likely to be used as identifiers in a program. This is not a complete list of keywords.


    alias

    and

    ansi

    as

    assembly

    auto

    base

    bool

    boolean

    byte

    call

    case

    catch

    char

    class

    const

    current

    date

    decimal

    declare

    default

    delegate

    dim

    do

    double

    each

    else

    elseif

    end

    enum

    erase

    error

    eval

    event

    exit

    extends

    finalize

    finally

    float

    for

    friend

    function

    get

    goto

    handles

    if

    implements

    import

    imports

    in

    inherit

    inherits

    instanceof

    int

    integer

    interface

    is

    let

    lib

    like

    lock

    long

    loop

    me

    mod

    module

    namespace

    new

    next

    not

    nothing

    null

    object

    on

    or

    overloads

    override

    overrides

    package

    private

    property

    protected

    public

    raise

    readonly

    redim

    rem

    resume

    return

    select

    self

    set

    shared

    short

    single

    static

    step

    stop

    string

    structure

    sub

    synchronize

    synchronized

    then

    this

    throw

    to

    try

    typeof

    unlock

    until

    use

    uses

    using

    var

    void

    volatile

    when

    while

    with

    xor

    FALSE

    TRUE

      

  • Do not use abbreviations in identifiers (including parameter names).

  • If you must use abbreviations, do use camelCasing for any abbreviation over two characters long, even if this is not the standard abbreviation.

D.1.3 Case Sensitivity

Do not use names that require case sensitivity. Components must be fully usable in both case-sensitive and case-insensitive languages. Since case-insensitive languages cannot distinguish between two names within the same context that differ only by case, components must avoid this situation.

  • Do not have two namespaces whose names differ only by case:

     
     namespace ee.cummings; namespace Ee.Cummings; 
  • Do not have a function with two parameters whose names differ only by case:

     
     void foo(string a, string A) 
  • Do not have a namespace with two types whose names differ only by case:

     
     System.Drawing.Point p; System.Drawing.POINT pp; 
  • Do not have a type with two properties whose names differ only by case:

     
     int Foo {get, set}; int FOO {get, set}; 
  • Do not have a type with two methods whose names differ only by case:

     
     void foo(); void Foo(); 
D.1.4 Avoiding Type Name Confusion

Different languages use different terms to identify the fundamental managed types. Designers must avoid using language-specific terminology. Follow the rules described in this section to avoid type name confusion.

  • Do use semantically interesting names rather than type names.

  • In the rare case that a parameter has no semantic meaning beyond its type, use a generic name. For example, a class that supports writing a variety of data types into a stream might have the following methods.

     
     void Write(double value); void Write(float value); void Write(long value); void Write(int value); void Write(short value); 
  • The above example is preferred to the following language-specific alternative:

     
     void Write(double doubleValue); void Write(float floatValue); void Write(long longValue); void Write(int intValue); void Write(short shortValue); 

In the extremely rare case that it is necessary to have a uniquely named method for each fundamental data type, do use the following universal type names:

C# Type Name

ilasm Representation

Universal Type Name

sbyte

int8

SByte

byte

unsigned int8

Byte

short

int16

Int16

ushort

unsigned int16

UInt16

int

int32

Int32

uint

unsigned int32

UInt32

long

int64

Int64

ulong

unsigned int64

UInt64

float

float32

Single

double

float64

Double

bool

int32

Boolean

char

unsigned int16

Char

string

System.String

String

object

System.Object

Object

A class that supports reading a variety of data types from a stream might have the following methods:

 
 double ReadDouble(); float ReadSingle(); long ReadInt64(); int ReadInt32(); short ReadInt16(); 

The above example is preferred to the following language-specific alternative:

 
 double ReadDouble(); float ReadFloat(); long ReadLong(); int ReadInt(); short ReadShort(); 
D.1.5 Namespaces

The following example illustrates the general rule for naming namespaces:

 
 CompanyName.TechnologyName 

Therefore, we should expect to see namespaces like the following:

 
 Microsoft.Office PowerSoft.PowerBuilder 
  • Do avoid the possibility of two published namespaces having the same name, by prefixing namespace names with a company name or other well-established brand. For example, Microsoft.Office for the Office Automation Classes provided by Microsoft.

  • Do use PascalCasing, and separate logical components with periods (for example, Microsoft.Office.PowerPoint). If your brand employs non-traditional casing, do follow the casing defined by your brand, even if it deviates from normal namespace casing (for example, NeXT.WebObjects and ee.cummings).

  • Do use plural namespace names where appropriate. For example, use System. Collections not System.Collection. Exceptions to this rule are brand names and abbreviations. For example, use System.IO not System.IOs.

  • Do not specify the same name for namespaces and classes. For example, do not use Debug for a namespace name and also provide a class named Debug.

D.1.6 Classes
  • Do name classes with nouns or noun phrases.

  • Do use PascalCasing.

  • Do use abbreviations in class names sparingly.

  • Do not use any type of class prefix (such as "C").

  • Do not use the underscore character.

  • Occasionally, it is necessary to have a class name that begins with "I", that is not an interface. This is acceptable as long as the character that follows "I" is lower case (for example, IdentityStore).

The following are examples of correctly named classes:

 
 public class FileStream { } public class Button { } public class String { } 
D.1.7 Interfaces
  • Do name interfaces with nouns or noun phrases, or adjectives describing behavior. For example, IComponent (descriptive noun), ICustomAttributeProvider (noun phrase), and IPersistable (adjective) are appropriate interface names.

  • Do use PascalCasing.

  • Do use abbreviations in interface names sparingly.

  • Do not use the underscore character.

  • Do prefix interface names with the letter "I", to indicate that the type is an interface.

  • Do use similar names when defining a class/interface pair where the class is a standard implementation of the interface. The names should differ only by the letter "I" prefix on the interface name.

The following example illustrates these guidelines for the interface IComponent and its standard implementation, the class Component:

 
 public interface IComponent { } public class Component : IComponent { } public interface IServiceProvider { } public interface IFormattable { } 
D.1.8 Attributes
  • Do add the Attribute suffix to custom attribute classes as in the following example:

     
     public class ObsoleteAttribute { } 
D.1.9 Enums
  • Do use PascalCasing for an enum type.

  • Do use PascalCasing for an enum value name.

  • Do use abbreviations in enum names sparingly.

  • Do not use a prefix on enum names (for example, "adXXX" for ADO enums, "rtfXXX" for rich text enums, etc.).

  • Do not use an "Enum" suffix on enum types.

  • Do use a singular name for an enum.

  • Do use a plural name for bit fields.

D.1.10 Fields
  • Do use camelCasing (except for static fields; see Partition V, section D.1.10.1).

  • Do not abbreviate field names.

    Spell out all the words used in a field name. Only use abbreviations if developers generally understand them. Do not use uppercase letters for field names. For example:

     
     class Foo {    string url;    string destinationUrl; } 
  • Do not use Hungarian notation for field names. Good names describe semantics, not type.

  • Do not use a prefix for field names.

  • Do not include a prefix on a field name for example, "g_" or "s_" to distinguish static versus non-static fields.

D.1.10.1 Static Fields
  • Do name static fields with nouns, noun phrases, or abbreviations for nouns.

  • Do not use a prefix for static field names.

  • Do name static fields with PascalCasing.

  • Do not prefix static field names with Hungarian type notation.

D.1.11 Parameter Names
  • Do use descriptive parameter names. Parameter names should be descriptive enough that in most scenarios the name of the parameter and its type can be used to determine its meaning.

  • Do name parameters with camelCasing.

  • Do use names based on a parameter's meaning rather than names based on the parameter's type. We expect development tools to provide the information about type in a useful manner, so the parameter name can be put to better use describing semantics rather than type. Occasional use of type-based parameter names is entirely appropriate.

  • Do not use reserved parameters. If more data is needed in the next version, a new overload can be added.

  • Do not prefix parameter names with Hungarian type notation.

     
     Type GetType (string typeName) string Format (string format, object [] args) 
D.1.12 Method Names
  • Do name methods with PascalCasing as in the following examples:

     
     RemoveAll() GetCharArray() Invoke() 
  • Do not use Hungarian notation.

  • Do name methods with verbs or verb phrases.

D.1.13 Property Names
  • Do name properties using a noun or noun phrase.

  • Do name properties with PascalCasing.

  • Do not use Hungarian notation.

D.1.14 Event Names
  • Do name events using PascalCasing.

  • Do not use Hungarian notation.

  • Do name event handlers (delegate types) with the "EventHandler" suffix, as in the following example:

     
     public delegate void MouseEventHandler(object sender, MouseEvent e); 
  • Consider using two parameters named sender and e.

    The sender parameter represents the object that raised the event. The sender parameter is always of type object, even if it is possible to employ a more specific type.

    The state associated with the event is encapsulated in an instance of an event class named e. Use an appropriate and specific event class for its type.

     
     public delegate void MouseEventHandler(object sender, MouseEvent e); 
  • Do name event argument classes with the "EventArgs" suffix, as in the following example:

     
     public class MouseEventArgs : EventArgs {    int x;    int y;    public MouseEventArgs(int x, int y) { this.x = x; this.y = y; }    public int X { get { return x; } }    public int Y { get { return y; } } } 
  • Do name event names that have a concept of pre and post using the present and past tense (do not use the BeforeXxx\AfterXxx pattern). For example, a close event that can be canceled would have a Closing and Closed event.

  • Consider naming events with a verb.

D.2 Type Member Usage Guidelines

D.2.1 Property Usage Guidelines
  • Do see Partition V, section D.2.1.1 on choosing between properties and methods.

  • Do not use properties and types with the same name.

    Defining a property with the same name as a type can cause ambiguity in some programming languages. It is best to avoid this ambiguity unless there is a clear justification for not doing so.

  • Do preserve the previous value if a property set throws an exception.

  • Do allow properties to be set in any order. Properties should be stateless with respect to other properties.

    It is often the case that a particular feature of an object will not take effect until the developer specifies a particular set of properties, or until an object has a particular state. Until the object is in the correct state, the feature is not active. When the object is in the correct state, the feature automatically activates itself without requiring an explicit call. The semantics are the same regardless of the order in which the developer sets the property values or how the developer gets the object into the active state.

D.2.1.1 Properties vs. Methods

Library designers sometimes face a decision between a property and a method. Use the following guidelines to help you choose between these options. The philosophy here is that users will think of properties as though they were fields, hence methods are preferred where the intuitive semantics or performance differ from those of fields.

  • Do use a property if the member has a logical backing store.

  • Do use a method in the following situations.

    • The operation is a conversion (such as Object.ToString()).

    • The operation is expensive (orders of magnitude slower than a field set would be).

    • Obtaining a property value using the Get accessor has an observable side effect.

    • Calling the member twice in succession results in different results.

    • The order of execution relative to other properties is important.

    • The member is static but returns a mutable value.

    • The member returns an array.

      Properties that return arrays can be very misleading. Usually it is necessary to return a copy of the internal array so that the user cannot change internal state. This, coupled with the fact that a user could easily assume it is an indexed property, leads to inefficient code. In the following example, each call to the Methods property creates a copy of the array. That would be 2n+1 copies for this loop.

       
       Type type = //get a type somehow for (int i = 0; i < type.Methods.Length; i++) {    if (type.Methods[i].Name.Equals ("foo"))    {...} } 
D.2.1.2 Read-Only and Write-Only Properties
  • Do use read-only properties when the user cannot change the logical backing data field.

  • Do not use write-only properties.

D.2.1.3 Indexed Property Usage
  • Do use only one indexed property per class, and make it the default indexed property for that class.

  • Do not use non-default indexed properties.

  • Do use the name "Item" for indexed properties unless there is an obviously better name (for example, a Chars property on string is better than Item).

  • Do use indexed properties when the logical backing store is an array.

  • Do not provide both indexed properties and methods that are semantically equivalent to two or more overloaded methods.

     
     MethodInfo Type.Method[string name]       ;; Should be method MethodInfo Type.GetMethod (string name, boolean ignoreCase) 
D.2.2 Event Usage Guidelines
  • Do use the "raise" terminology for events rather than "fire" or "trigger" terminology.

  • Do use a return type of void for event handlers.

  • Do make event classes extend the class System.EventArgs.

  • Do implement AddOn<EventName> and RemoveOn<EventName> for each event.

  • Do use a family virtual method to raise each event.

    This is not appropriate for sealed classes, because classes cannot be derived from them. The purpose of the method is to provide a way for a derived class to handle the event using an override. This is more natural than using delegates in the case where the developer is creating a derived class.

    The derived class can choose not to call the base during the processing of On<EventName>. Be prepared for this by not including any processing in the On<EventName> method that is required for the base class to work correctly.

  • Do assume that anything can go in an event handler.

    Classes are ready for the handler of the event to do almost anything, and in all cases the object is left in a good state after the event has been raised. Consider using a try/finally block at the point where the event is raised. Since the developer can call back on the object to perform other actions, do not assume anything about the object state when control returns to the point at which the event was raised

D.2.3 Method Usage Guidelines
  • Do use non-virtual methods unless overriding is intended by the design. Providing the ability to override a method (i.e., making the method virtual) implies that the design of the type is independent of details of the method's implementation; this is rarely true without careful design of the type.

  • Do use method overloading when you provide different methods that do semantically the same thing.

  • Do favor method overloading to default arguments. Default arguments are not allowed in the Common Language Specification (CLS).

     
     int String.IndexOf (String name); int String.IndexOf (String name, int startIndex); 
  • Do use default values correctly.

    In a family of overloaded methods the complex method should use parameter names that indicate a change from the default state assumed in the simple method.

    For example, in the code below, the first method assumes the look-up will not be case-sensitive. In method two, we use the name ignoreCase rather than caseSensitive because the former indicates how we are changing the default behavior.

     
     MethodInfo Type.GetMethod(String name);  //ignoreCase = false MethodInfo Type.GetMethod (String name, boolean ignoreCase); 

    It is very common to use a zeroed state for the default value (such as: 0, 0.0, false, "", etc.).

  • Do be consistent in the ordering and naming of method parameters.

    It is common to have a set of overloaded methods with an increasing number of parameters to allow the developer to specify a desired level of information. The more parameters specified, the more detail that is specified. All the related methods have a consistent parameter order and naming pattern. Each of the method variations has the same semantics for their shared set of parameters.

    This consistency is useful even if the parameters have different types.

    The only method in such a group that should be virtual is the one that has the most parameters.

  • Do use method overloading for variable numbers of parameters.

    Where it is appropriate to have variable numbers of parameters to a method, use the convention of declaring N methods with increasing numbers of parameters, and also provide a method which takes an array of values for numbers greater than N. N = 3 or N = 4 is appropriate for most cases. Only the method that takes the array should be virtual.

  • Do make only the most complete overload virtual (if extensibility is needed) and define the other operations in terms of it.

     
     public int IndexOf (string s) { return IndexOf (s, 0); } public int IndexOf (string s, int start) { return IndexOf (s, startIndex, s.Length); } public virtual int IndexOf (string s, int start, int count) { //do real work } 
  • Do use the ParamsAttribute pattern for defining methods with a variable number of arguments.

     
     void Format (string formatString, params object [] args) 
  • Consider using the varargs ("…") calling convention to provide a variable number of arguments, but do not use this without providing an alternative mechanism to accomplish the same thing, since it is not CLS-compliant.

  • Consider providing special-case code for a small number of arguments to a method that takes a variable number of arguments, but only where the performance gained is significant. When this approach is taken, it becomes difficult to allow the method to be overridden because all the special cases must be overridden as well.

D.2.4 Constructor Usage Guidelines
  • Do have only a default private constructor (or no constructor at all) if there are only static methods and properties on a class.

  • Do minimal work in the constructor.

  • Do provide a family constructor that can be used by types in a derived class.

  • Do not provide an empty default constructor for value types.

  • Do use parameters in constructors as shortcuts for setting properties.

    There should be no difference in semantics between using the empty constructor followed by some calls to property setters, and using a constructor with multiple arguments.

  • Do be consistent in the ordering and naming of constructor parameters.

    A common pattern for constructor parameters is to provide an increasing number of parameters to allow the developer to specify a desired level of information. The more parameters that are specified, the more detail that is specified. For all of the following constructors, there is a consistent order and naming of the parameters.

D.2.5 Field Usage Guidelines
  • Do not use instance fields that are public or family.

  • Consider providing get and set property accessors for fields instead of making them public.

  • Do use a family property that returns the value of a private field to expose a field to a derived class.

    By not exposing fields directly to the developer, the class can be versioned more easily for the following reasons:

    1. A field cannot be changed to a property while maintaining binary compatibility.

    2. The presence of executable code in get and set property accessors allows later improvements, such as demand-creation of an object upon usage of the property, or a property change notification.

  • Do use readonly static fields instead of properties where the value is a global constant.

  • Do not use literal static fields if the value can change between versions.

  • Do use public static readonly fields for predefined object instances.

D.2.6 Parameter Usage Guidelines
  • Do check arguments for validity.

    Perform argument validation for every public or family method and property set accessor, and throw meaningful exceptions to the developer. The System.ArgumentException exception, or one of its subclasses, is used in these cases.

    Note that the actual checking does not necessarily have to happen in the public/family method itself. It could happen at a lower level in some private routines. The main point is that the entire surface area that is exposed to developers checks for valid arguments.

    Parameter validation should be performed before any side-effects occur.

D.3 Type Usage Guidelines

D.3.1 Class Usage Guidelines
  • Do favor using classes over any other type (i.e., interfaces or value types).

D.3.1.1 Base Class Usage Guidelines

Base classes are a useful way to group objects that share a common set of functionality. Base classes can provide a default set of functionality, while allowing customization though extension.

Add extensibility or polymorphism to your design only if you have a clear customer scenario for it.

  • Do use base classes rather than interfaces.

    From a versioning perspective, interfaces are less flexible than classes. With a class, you can ship Version 1.0 and then in Version 2.0 decide to add another method. As long as the method is not abstract (that is, as long as you provide a default implementation of the method), any existing derived classes continue to function unchanged.

    Because interfaces do not support implementation inheritance, the pattern that applies to classes does not apply to interfaces. Adding a method to an interface is like adding an abstract method to a base class: any class that implements the interface will break because the class does not implement the interface's new method.

    Interfaces are appropriate in the following situations:

    • Several unrelated classes want to support the protocol.

    • These classes already have established base classes.

    • Aggregation is not appropriate or practical.

    For all other cases, class inheritance is a better model. For example, make IByteStream an interface so a class can implement multiple stream types. Make ValueEditor an abstract class because classes derived from ValueEditor have no other purpose than to edit values.

  • Do provide customization through family methods.

    The public interface of a base class should provide a rich set of functionality for the consumer of that class. However, customizers of that class often want to implement the fewest methods possible to provide that rich set of functionality to the consumer. To meet this goal, provide a set of non-virtual or final public methods that call through to a single family method with the "Impl" suffix that provides implementations for such a method. This pattern is also known as the "Template Method."

     
     Public Control { public void SetBounds(int x, int y, int width, int height)   { . . .     SetBoundsImpl (...);    }    public void SetBounds(int x, int y,                          int width, int height,                          BoundsSpecified specified)    { . . .      SetBoundsImpl (...);    }    protected virtual void SetBoundsImpl                         (int x, int y,                            int width, int height,                            BoundsSpecified specified)     { // Do the real work here.      }    } 
  • Do define a family constructor on all abstract classes. Many compilers will insert a public constructor if you do not. This can be very misleading to users, as it can only be called from derived classes.

D.3.1.2 Sealed Class Usage Guidelines
  • Do use sealed classes if creating derived classes will not be required.

  • Do use sealed classes if there are only static methods and properties on a class.

D.3.2 Value Type Usage Guidelines
  • Do use a value type for types that meet all of the following criteria:

    • Act like built-in types.

    • Have an instance size under 16 bytes.

    • Value semantics are desirable.

  • Do not provide a default constructor.

  • Do program assuming a state where all instance data [that] is set to zero, false, or null (as appropriate) is valid, since this will be the state if no constructor is run and there is no guarantee that a constructor will be run (unlike for classes).

D.3.2.1 Enum Usage Guidelines
  • Do use an Enum to strongly type parameters, property, and return type. This allows development tools to know the possible values for a property or parameter.

  • Do use the System.Flags custom attribute for an enum if a bitwise OR operation is to be performed on the numeric values.

  • Do use int32 as the underlying type of an enum.

    An exception to this rule is if the enum represents flags and there are many flags (>32) or the enum may grow to many flags in the future or the type needs to be different from type int32 for backward compatibility.

  • Do use an enum with flags attributes only if the value can be completely expressed as a set of bitflags. Do not use an enum for open sets (e.g., a version number).

  • Do not assume enum arguments will be in the defined range. Do argument validation.

  • Do favor using an enum over static final constants.

  • Do use int32 as the underlying type of an enum unless either of the following is true:

    1. The enum represents flags, and there are currently many flags (>32), or the enum may grow to many flags in the future.

    2. The type needs to be different from int for backward compatibility.

  • Do not use a non-integral enum type. Only use int8, int16, int32, or int64.

  • Do not define methods, properties, or events on an enum.

  • Do not use any suffix on enum types.

D.3.3 Interface Usage Guidelines
  • Do favor using classes over any other type (i.e., interfaces or value types).

  • Do use a class or abstract class in preference to an interface, where possible.

  • Do use interfaces to provide extensibility and the ability to customize.

  • Do provide a default implementation of an interface where it is appropriate. For example, System.Collections.DictionaryBase is the default implementation of the System.Collections.IDictionary interface.

  • Do see Partition V, section D.3.1.1 on the versioning issues with interfaces and abstract classes.

  • Do not use interfaces as empty markers. Use custom attributes instead.

    If you need to mark a class as having a specific attribute (such as immutable or serializable), use a custom attribute rather than an interface.

  • Do implement interfaces using MethodImpls (see Partition II [section 21.25]) and private virtual methods if you only want the interface methods available when cast to that interface. This is particularly useful when a class or value type implements an internal interface that is of no interest to a consumer of the class or value type.

D.3.4 Delegate Usage Guidelines

Delegates are a powerful tool that allow the managed code object model designer to encapsulate method calls. They are used in two basic areas.

Event Notifications:

See Partition V, section D.2.2 on event usage guidelines.

Callbacks:

Passed to a method so that user code can be called multiple times during execution to provide customization. The classic example of this is passing a Compare callback to a sort routine. These methods should use the Callback conventions

  • Do use an Event design pattern for events (even if it is not user interface related).

D.3.5 Attribute Classes

The CLI enables developers to invent new kinds of declarative information, to specify declarative information for various program entities, and to retrieve attribute information in a runtime environment. New kinds of declarative information are defined through the declaration of attribute classes, which may have positional and named parameters.

  • Do specify an AttributeUsage on your attributes to define their usage precisely.

  • Do seal attribute classes if possible.

  • Do provide a single constructor for the attribute.

  • Do use a parameter to the attribute's constructor when the value of that parameter is always required to make the attribute.

  • Do use a field on an attribute when the value of that property can be optionally specified to make the attribute.

  • Do not name a parameter to the constructor with the same name as a field or property of the attribute.

  • Do provide a read-only property with the same name (different casing) as each parameter to the constructor.

  • Do provide a read-write property with the same name (different casing) as each field of the attribute.

D.3.6 Nested Types

A nested type is a type defined within the scope of another type. Nested types are very useful for encapsulating implementation details of a type, such as an enumerator over a collection, because they can have access to private state. Public nested types are rarely used.

Do not use public nested types unless all of the following are true:

  • The nested type logically belongs to the containing type.

  • The nested type is not used very often, or at least not directly.

D.4 Error Raising and Handling

  • Do end Exception class names with the "Exception" suffix.

  • Do use these common constructors.

     
     public class XxxException : Exception {    XxxException() { }    XxxException(string message) { }    XxxException(string message, Exception inner) { } } 
  • Do use the predefined exception types. Only define new exception types for programmatic scenarios, meaning you expect users of your library to catch exceptions of this new type and perform a programmatic action based on the exception type.

  • Do not derive new exceptions directly from the base class Exception. Use one of its predefined subclasses instead.

  • Do use a localized description string. When the user sees an error message, it will be derived from the description string of the exception that was thrown, and never from the exception class. Include a description string in every exception.

  • Do use grammatically correct error messages, including ending punctuation.

    Each sentence in a description string of an exception should end in a period. This way, code that generically displays an exception message to the user does not have to handle the case where a developer forgot the final period, which is relatively cumbersome and expensive.

  • Do provide exception properties for programmatic access. Include extra information (besides the description string) in an exception only when there is a programmatic scenario where that additional information is useful.

  • Do throw exceptions only in exceptional cases.

    • Do not use exceptions for normal or expected errors.

    • Do not use exceptions for normal flow of control.

  • Do return null for extremely common error cases. For example, File.Open returns a null if the file is not found, but throws an exception if the file is locked.

  • Do design classes such that in the normal course of use there will never be an exception thrown. For example, a FileStream class might expose a way of determining if the end of the file has been reached to avoid the exception that will be thrown if the developer reads past the end of the file.

  • Do throw an InvalidOperationException if in an inappropriate state.

    The System.InvalidOperationException exception should be thrown if the property set or method call is not appropriate given the object's current state.

  • Do throw an ArgumentException or create an exception derived from this class if bad parameters are passed or detected.

  • Do realize that the stack trace starts at the point where an exception is thrown, not where it is created with the new operator. You should consider this when deciding where to throw an exception.

  • Do throw exceptions rather than return an error code.

  • Do throw the most specific exception possible.

  • Do set all the fields on the exception you use.

  • Do use Inner exceptions (chained exceptions).

  • Do clean up side-effects when throwing an exception. Clearly document cases where an exception may occur after a side-effect has already taken place and cannot be retracted.

  • Do not assume that side-effects do not occur before an exception is thrown, but rather that the state is restored if one is thrown. That is, another thread may see the side-effect, but will then see an additional one to restore the state.

D.4.1 Standard Exception Types

The following table breaks down the standard exceptions and the conditions for which you should create a derived class.

Exception Type

Base Type

Description

Example

Exception

Object

Base class for all exceptions.

None (use a derived class of this exception)

SystemException

Exception

Base class for all runtime-generated errors.

None (use a derived class of this exception)

IndexOutOfRangeException

SystemException

Thrown only by the runtime when an array is indexed improperly.

Indexing an array outside of its valid range: arr[arr.Length+1]

NullReferenceException

SystemException

Thrown only by the runtime when a null object is referenced.

object o = null; o.ToString();

InvalidOperationException

SystemException

Thrown by methods when in an invalid state.

Calling Enumerator.GetNext() after removing an item from the underlying collection.

ArgumentException

SystemException

Base class for all argument exceptions. Derived classes of this exception should be thrown where applicable.

None (use a derived class of this exception)

ArgumentNullException

ArgumentException

Thrown by methods that do not allow an argument to be null.

String s = null; "foo".IndexOf (s);

ArgumentOutOfRangeException

ArgumentException

Thrown by methods that verify that arguments are in a given range.

String s = "string"; s.Chars[9];

InteropException

SystemException

Base class for exceptions that occur or are targeted at environments outside of the runtime.

None (use a derived class of this exception)

D.5 Array Usage Guidelines

  • Do use a collection when Add, Remove, or other methods for manipulating the collection are supported. This scopes all related methods to the collection.

  • Do use collections to add read-only wrappers around internal arrays.

  • Do use collections to avoid the inefficiencies in the following code.

     
     for (int i = 0; i < obj.myObj.Count; i++)       DoSomething(obj.myObj[i]) 

    Also see Partition V, section D.2.1.1.

  • Do return an Empty array instead of a null.

    Users assume that the following code will work:

     
     public void DoSomething(...) { int a[] = SomeOtherFunc();   if (a.Length > 0) // Don't expect NULL here!   { // do something   } } 

D.6 Operator Overloading Usage Guidelines

  • Do define operators on Value types that are logically a built-in language type.

  • Do provide operator-overloading methods only involving the class in which the methods are defined.

  • Do use the names and signature conventions described in the Common Language Specification.

  • Do not be cute.

    Operator overloading is useful in cases where it is immediately obvious what the result of the operation will be. For example, it makes sense to be able to subtract one Time value from another Time value and get a TimeSpan. However, it is not appropriate to use shift to write to a stream.

  • Do overload operators in a symmetric fashion. For example, if you overload the Equal operator (==), you should also overload the not equal (!=) operator.

  • Do provide alternative signatures.

    Most languages do not support operator overloading. For this reason it is a CLS requirement that you include a method with an appropriate domain-specific name that has the equivalent functionality, as in the following example.

     
     class Time {    TimeSpan operator -(Time t1, Time t2) { }    TimeSpan Difference(Time t1, Time t2) { } } 

    See Partition I, section 10.3.

D.6.1 Implementing Equals and operator==
  • Do see the section on implementing the Equals method in Partition V, section D.7.

  • Do implement GetHashCode() whenever you implement Equals(). This keeps Equals() and GetHashCode() synchronized.

  • Do override Equals whenever you implement operator== and make them do the same thing. This allows infrastructure code such as Hashtable and ArrayList, which use Equals(), to behave the same way as user code written using operator==.

  • Do override Equals anytime you implement IComparable.

  • Consider implementing operator overloading for ==, != , <, and > when you implement IComparable.

  • Do not throw exceptions from Equals(), GetHashCode(), or operator== methods.

D.6.1.1 Implementing operator== on Value Types
  • Do overload operator== anytime equality is meaningful, because in most programming languages there is no default implementation of operator== for value types.

  • Consider implementing Equals() on ValueTypes because the default implementation on System.ValueType will not perform as well as your custom implementation.

  • Do implement operator== anytime you override Equals().

D.6.1.2 Implementing operator== on Reference Types
  • Do use care when implementing operator== on reference types. Most languages do provide a default implementation of operator== for reference types; therefore, overriding the default implementation should be done with care. Most reference types, even those that implement Equals() should not override operator==.

  • Do override operator== if your type has value semantics (that is, if it looks like a base type such as a Point, String, BigNumber, etc.). Anytime you are tempted to overload + and , you also should consider overloading operator==.

D.6.2 Cast Operations (op_Explicit and op_Implicit)
  • Do not lose precision in implicit casts.

    For example, there should not be an implicit cast from Double to Int32, but there may be one from Int32 to Int64.

  • Do not throw exceptions from implicit casts because it is very difficult for the developer to understand what is happening.

  • Do provide cast operations that operate on the whole object. The value that is cast represents the whole value being cast, not one sub-part. For example, it is not appropriate for a Button to cast to a string by returning its caption.

  • Do not generate a semantically different value.

    For example, it is appropriate to convert a Time or TimeSpan into an Int. The Int still represents the time or duration. It does not make sense to convert a file name string such as, "c:\mybitmap.gif" into a Bitmap object.

  • Do not provide cast operations for values between different semantic domains. For example, it makes sense that an Int32 can cast to a Double. It does not make sense for an Int to cast to a String, because they are in different domains.

D.7 Equals

Do see Partition V, section D.6.1 on implementing operator==.

Do override GetHashCode() in order for the type to behave correctly in a hashtable.

Do not throw an exception in your Equals implementation. Return false for a null argument, etc.

Do follow the contract defined on Object.Equals.

  • x.Equals(x) returns true.

  • x.Equals(y) returns the same value as y.Equals(x).

  • (x.Equals(y) && y.Equals(z)) returns true if and only if x.Equals(z) returns true.

  • Successive invocations of x.Equals(y) return the same value as long as the objects referenced by x and y are not modified.

  • x.Equals(null) returns false.

For some kinds of objects, it is desirable to have Equals test for value equality instead of referential equality. Such implementations of Equals return true if the two objects have the same value, even if they are not the same instance. The definition of what constitutes an object's value is up to the implementer of the type, but it is typically some or all of the data stored in the instance variables of the object. For example, the value of a string is based on the characters of the string; the Equals method of the String class returns true for any two string instances that contain exactly the same characters in the same order.

When the Equals method of a base class provides value equality, an override of Equals in a class derived from that base class should invoke the inherited implementation of Equals.

If you choose to overload the equality operator for a given type, that type should override the Equals method. Such implementations of the Equals method should return the same results as the equality operator. Following this guideline will help ensure that class library code using Equals (such as ArrayList and Hashtable) behaves in a manner that is consistent with the way the equality operator is used by application code.

If you are implementing a value type, you should follow these guidelines:

  • Consider overriding Equals to gain increased performance over that provided by the default implementation of Equals on System.ValueType.

  • If you override Equals and the language supports operator overloading, you should overload the equality operator for your value type.

If you are implementing reference types, you should follow these guidelines:

  • Consider overriding Equals on a reference type if the semantics of the type are based on the fact that the type represents some value(s). For example, reference types such as Point and BigNumber should override Equals.

  • Most reference types should not overload the equality operator, even if they override Equals. However, if you are implementing a reference type that is intended to have value semantics, such as a complex number type, you should override the equality operator.

If you implement IComparable on a given type, you should override Equals on that type.

ANNOTATION

This section on Equals is very worth a careful reading by anyone who is adding new data types to the system. Equality is very complicated, the rules are confusing, and violating them leads to unusual behavior in unexpected places. In C#, for example, a==b checks the identity of the two reference types, but if you overloaded the values, it checks those overloaded types. If you need to check object identity, you must use "object.reference =", which will work, where "==" will not. Not all are enthusiastic about these rules.


D.8 Callbacks

Delegates, Interfaces, and Events can each be used to provide callback functionality. Each has its own specific usage characteristics that make it better suited to particular situations.

Use Events if the following are true:

  • One signs up for the callback up front (typically through separate Add and Remove methods).

  • Typically more than one object will care.

Use a Delegate if the following are true:

  • You want a C-style function pointer.

  • Single callback.

  • Registered in the call or at construction time (not through separate Add method).

Use an Interface if the following is true:

  • The callback entails complex behavior.

D.9 Security in Class Libraries

Class library authors need to consider two perspectives with respect to security. Whether these perspectives are applicable will depend upon the class itself. Some classes, such as System.IO.FileStream represent objects that need protection with permissions; the implementation of these classes is responsible for checking the appropriate permissions of the caller required for each action and only allowing authorized callers to perform the actions for which they have permission. The System.Security namespace contains some classes to help make these checks easier. Additionally, class library code often is fully trusted, or at least highly trusted, code. Any flaws in the code represent a serious threat to the integrity of the entire security system. Therefore, extra care is required when writing class library code, as detailed below.

  • Do access protected resources only after checking the permissions of your callers, through either a declarative security attribute or an explicit call to Demand on an appropriate security permission object.

  • Do assert a permission only when necessary, and always precede it by the necessary checks.

  • Do not assume that code will only be called by callers with certain permissions.

  • Do not define non-typesafe interfaces that might be used to bypass security.

  • Do not expose functionality that allows a semi-trusted caller to take advantage of higher trust of the class.

D.10 Threading Design Guidelines

  • Do not provide static methods that mutate static state.

    In common server scenarios, static state is shared across requests, which means multiple threads can execute that code at the same time. This opens up the possibility for threading bugs. Consider using a design pattern that encapsulates data into instances that are not shared.

  • Do not normally provide thread-safe instance state.

    By default, the library is not thread-safe. Adding locks to create thread-safe code decreases performance and increases lock contention (as well as opening up deadlock bugs). In common application models, only one thread at a time executes user code, which minimizes the need for thread safety. In cases where it is interesting to provide a thread-safe version, a GetSynchronized() method can be used to return a thread-safe instance of that type. (See System.Collections, which is described in the .NET Framework Standard Library Annotated Reference, for examples.)

  • Do make all static state thread-safe.

    If you must use static state, make it thread-safe. In common server scenarios, static data is shared across requests, which means multiple threads can execute that code at the same time. For this reason it is necessary to protect static state.

  • Do be aware of non-atomic operations.

    Value types whose underlying representations are greater than 32 bits may have non-atomic operations. Specifically, because value types are copied bitwise (by value as opposed to by reference), race conditions can occur in what appear to be straightforward assignments within code.

    For example, consider the following code (executing on two separate threads), where the variable x has been declared as type Int64.

     
     // Code executing on Thread "A". x = 54343343433; // Code executing on Thread "B". x = 934343434343; 

    At first glance it seems to indicate that there is no possibility of race conditions (since each line looks like a straight assignment operation). However, because the underlying variable is a 64-bit value type, the actual code is not doing an atomic assignment operation. Instead, it is doing a bitwise copy of two 32-bit halves. In the event of a context switch, halfway during the value type assignment operation on one of the threads, the resulting x variable can have corrupt data (for example, the resulting value will be composed of 32 bits of the first number, and 32 bits of the second number).

  • Do be aware of method calls in locked sections.

    Deadlocks can result when a static method in class A calls static methods in class B, and vice versa. If A and B both synchronize their static methods, this will cause a deadlock. You might only discover this deadlock under heavy threading stress.

    Performance issues can result when a static method in class A calls a static method in class A. If these methods are not factored correctly, performance will suffer because there will be a large amount of redundant synchronization. Excessive use of fine-grained synchronization might negatively impact performance. In addition, it might have a significant negative impact on scalability.

  • Do be aware of issues with the lock statement and consider using System.Threading.Interlocked instead.

    It's tempting to use the lock statement in C# to solve all threading problems. But the System.Threading.Interlocked class is superior for updates that must be made automically.

  • Do avoid the need for synchronization if possible.

    Obviously for high-traffic pathways it is nice to avoid synchronization. Sometimes the algorithm can be adjusted to tolerate races rather than eliminating them.

ANNOTATION

It is important to understand the threading guidelines. The design philosophy in this regard of the CLI is dfferent for some other libraries. For the most part, the CLI class libraries are not written to do locking, because general-purpose locking leads to inefficiencies that are imposed on everyone for programs that work in a multi-threaded environment. Instead, it was determined to first ensure that the libraries are efficient, and to provide simple ways to use locks. Thus, those who are in multi-threaded environments can use locks without building locking into the libraries.




The Common Language Infrastructure Annotated Standard (Microsoft. NET Development Series)
The Common Language Infrastructure Annotated Standard (Microsoft. NET Development Series)
ISBN: N/A
EAN: N/A
Year: 2002
Pages: 121

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net