Class Design
Member Accessibility
When declaring classes, methods and fields,
do not rely upon the default member accessibility. Instead, explicitly decorate each with
“public”, “private”, etc. as necessary.
This will aid maintainability, as other developers may not be familiar
with default accessibility.
Constructors
If you do not specify at least one
non-static constructor, an empty parameterless public constructor is created
automatically during compilation.
Relying on this behavior is a dangerous practice. If another developer later adds an explicit
constructor accepting parameters, the compiler will no longer include the
default parameterless constructor – this will break any existing client code
which relied upon the default constructor.
Always explicitly code the parameterless
constructor if it is needed. If it is
not needed and you do not have any other constructors which accept parameters,
create the parameterless one but mark it as private. [For more information on classes with no
constructors, see Static Classes
on page 2.]
Example – Explicitly adding parameterless
constructor, not relying on default
public class Customer
{
public Customer()
{
// Add implementation
}
}
{
public Customer()
{
// Add implementation
}
}
Example – Marking the parameterless
constructor private when no constructors should exist
public class Customer
{
private Customer() {}
}
{
private Customer() {}
}
Example – Not including the parameterless
constructor because another constructor exists
public class Customer
{
public Customer(string firstName, stringLastName)
{
// Implementation
}
{
public Customer(string firstName, stringLastName)
{
// Implementation
}
}
Property Order Dependencies
Do not write classes which expect property A
to be set before setting B - properties of classes must be able to be
assigned to in any order. Ensure the
class’s constructor returns the instance in a state ready for such use.
Public and Protected Virtual
Methods
In non-sealed classes, you should generally
mark any public or protected methods as virtual. This allows straightforward specialization of
the class without having to resort to the new keyword, which results in
different (and generally misunderstood) behavior.
Using Structures
Consider using a structure instead of a
class when you wish to model a small set of value types and optionally a minor
amount behavior around those values.
Think of a structure as a small and fast value type data container. As structures are created on the stack they
will yield performance advantages over classes when used properly.
That same performance advantage gained by
using the stack might become a liability when too many fields are contained
within the same struct. When too many
fields are used, a point of diminishing returns may be reached where the system
spends more time managing the stack to access struct values than it would using
the direct references to values that a class provides. While there is no absolute number defining
how many fields are “too many”, keep this potential behavior in mind when
designing your systems.
As structs do not support inheritance
(although they can implement interfaces), if there is a chance that what you
are modeling will grow to serve a more complex purpose, use a class instead.
Additionally, structs lose performance
advantages when used in collections because collections access their entries as
reference types. Therefore, each access
to a struct will require boxing operations – these generally outweigh the
stack-based benefits a struct provides.
Overriding Object.ToString()
When implementing a new class, remember to
implement an override for Object.ToString()
to render a meaningful text representation of the current object instance. Often, this string represents the state of
the instance for debugging/logging purposes.
Document what it returns using a XML documentation.
Example:
namespace Examples
{
public class Customer
{
{
private string _name;
...
///<summary>
///Returns the current Customer name.
///</summary>
...
///<summary>
///Returns the current Customer name.
///</summary>
public override string
ToString()
{
return _name;
}
return _name;
}
}
}
}
The default Object implementation of
ToString() returns the full name of
the class. From the above example
Customer class, an instance rendered with Console.Write(customer)
would have printed “Examples.Customer” without the ToString() override,
but with the override now prints the string assigned to the _name field.
Object Comparison
If ordering or comparison of instances of
your class would make sense, be certain to implement the IComparable
interface. Implement IComparable.CompareTo()
and overload the operators <, >, <= and >= (in terms of your CompareTo
implementation.)
Also, consider overriding Object.Equals()
(and the == and != operators) if you implement the IComparable interface. (See Object Equality below.)
Example:
public class SomeClass : IComparable
{
private int _someID;
{
private int _someID;
int
IComparable.CompareTo(object o)
{
return (_someID – ((SomeClass)o)._someID);
}
public static bool operator < (SomeClass lhs, SomeClass rhs)
return (_someID – ((SomeClass)o)._someID);
}
public static bool operator < (SomeClass lhs, SomeClass rhs)
{
return ((IComparable)lhs).CompareTo(rhs) < 0;
}
return ((IComparable)lhs).CompareTo(rhs) < 0;
}
public static bool
operator > (SomeClass lhs, SomeClass rhs)
{
return ((IComparable)lhs).CompareTo(rhs) > 0;
}
return ((IComparable)lhs).CompareTo(rhs) > 0;
}
public static bool
operator <= (SomeClass lhs, SomeClass rhs)
{
return ((IComparable)lhs).CompareTo(rhs) <= 0;
}
return ((IComparable)lhs).CompareTo(rhs) <= 0;
}
public static bool
operator >= (SomeClass lhs, SomeClass rhs)
{
return ((IComparable)lhs).CompareTo(rhs) >= 0;
}
}
return ((IComparable)lhs).CompareTo(rhs) >= 0;
}
}
[Note:
This example uses an explicitly implemented interface to reduce method
clutter. For details on this approach,
see Explicit Interface Implementation
on page 29.]
Object Equality
If the concept of equivalence applies to
instances of your class, consider overriding Object.Equals() and the equality
(==) and inequality (!=) operators.
Observe the following when doing so:
- If you override the behavior of the equality (==) operator,
also override Object.Equals().
- x.Equals(x) must return true.
- x.Equals(y) must return the same value as y.Equals(x).
- If x.Equals(y) and y.Equals(z) both return true, x.Equals(z) must also return true.
- Implement Object.GetHashCode() whenever implementing Object.Equals()
(VS.Net will flag a warning reminder if you do not.) Equal instances should return the same
hash code from GetHashCode().
Ideally, the hash value should be based on immutable values (so the
hash remains the same over the life of the instance, regardless of changes
to its state.)
[For details on implementing
GetHashCode(), see VS.Net help under “Object.GetHashCode Method”, or pages
162-4 of Applied Microsoft .Net Framework Programming by Richter.]
Equality of Value Types
For value type classes, override Object.GetHashCode(),
then override Object.Equals() using an underlying call to a type-safe version
of Equals to avoid boxing overhead.
Example:
public class ValueType
{
private
<ReferenceOrValueType> _data;
public override int
GetHashCode()
{
return
_data.GetHashCode();
}
public override bool
Equals(object o)
{
if (!(o is ValueType))
{
// Not an instance of ValueType, cannot be equal
if (!(o is ValueType))
{
// Not an instance of ValueType, cannot be equal
return false;
}
// Call type-safe version with casted reference
}
// Call type-safe version with casted reference
return
this.Equals((ValueType)o);
}
}
public bool
Equals(ValueType rhs)
{
return
_data.Equals(rhs._data); // If _data is
a value type
/* --OR-- */
return Object.Equals(_data, rhs._data); // If _data is a reference type
/* --OR-- */
return Object.Equals(_data, rhs._data); // If _data is a reference type
}
public static bool operator==(ValueType lhs, ValueType rhs)
public static bool operator==(ValueType lhs, ValueType rhs)
{
return lhs.Equals(rhs); // Evaluate using Equals override
return lhs.Equals(rhs); // Evaluate using Equals override
}
public static bool operator!=(ValueType lhs, ValueType rhs)
public static bool operator!=(ValueType lhs, ValueType rhs)
{
return !lhs.Equals(rhs); // Evaluate using Equals override
return !lhs.Equals(rhs); // Evaluate using Equals override
}
}
}
Equality of Reference Types
As there are default implementations for
the equality (==) operator with reference types, you should use caution when
overriding equality on reference types.
Most reference types that define equivalence as whether two pointers
reference the same instance should not override the equality operator (even if
they do override Object.Equals().)
However, if you are implementing a
reference type that defines equality based on values each instance contains,
rather than the testing whether two references point to the same instance, you
should override the equality operator.
This is (essentially) how the String class (a reference type with value
type semantics) behaves.
[For a complete discussion of object equality,
see “Object Equality and Inequality” in Applied Microsoft .Net Framework
Programming by Richter.]
Exposing Reference Types Safely
When a class exposes a reference type via a
property, you must be aware that changes may be made to that instance by the
calling code. For example, consider a
property, “List” that exposes a private ArrayList instance.
private ArrayList _members;
public ArrayList Members
{
get {return _members;}
set {_members = value;}
public ArrayList Members
{
get {return _members;}
set {_members = value;}
}
This property basically allows any
modifications to be made to that instance – even if the “set” is removed! The issue is that once the calling code gets
the reference/pointer, any action taken, such as “o.Members.Add(something)”,
will affect that private _members ArrayList. In this case, the user could add an object of
any type, not just a “Member” instance, or even null.
It is up to the class designer to determine
when and to what extent private reference types exposed via properties (or
returned by methods) need to be safeguarded.
Any countermeasures must be fully documented in the class’ XML
documentation, so a developer utilizing the class is aware of the behavior.
Some potential countermeasures:
- Clone the reference type and return the copy via the property
[see Object
Cloning below
for details.] The property should
not have a “set”, and in fact probably shouldn’t be a property at
all. Consider creating a method
with a helpful name, such as “GetMemberListCopy()”.
- Some reference types enable read-only modes. For example, the ArrayList class exposes
a static ReadOnly() method that is used to return a wrapper of the
given ArrayList that prevents changes.
The above example would become:
Private ArrayList _members;
public ArrayList Members
{
get {return ArrayList.ReadOnly(_members);}
// set {_members = value;}
public ArrayList Members
{
get {return ArrayList.ReadOnly(_members);}
// set {_members = value;}
}
- Do not expose the reference type directly via a property or
method at all. Expose only “helper”
properties or methods that allow indirect/monitored access to the state of
the internal reference type. For
example, you could add methods like “GetMember()” and “AddMember(…)” that
allow monitored access to the private ArrayList in the above example.
Object Cloning
If your class needs to support the copying
of one instance into a new instance, you should implement the ICloneable
interface. You must decide and document
whether the Clone() method returns a shallow or deep copy of the
instance. A shallow copy simply
duplicates value types and if any reference types are in the object, the
reference (pointer) is duplicated. A
deep copy duplicates both value types and the contents of any contained
reference types. A shallow copy may
result in instances with shared reference data – changing one would affect the
other. By contrast, a deep copy results
in two instances that share nothing.
If you choose to support shallow copies,
implement Clone() in terms of a call to Object.MemberwiseClone().
Shallow Copy Clone Example:
public class SomeClass : ICloneable
{
{
private int _value;
private string _someString;
private Hashtable _ht;
///<summary>
///Returns a new instance that is a shallow copy of the current instance
///</summary>
public object Clone()
{
private string _someString;
private Hashtable _ht;
///<summary>
///Returns a new instance that is a shallow copy of the current instance
///</summary>
public object Clone()
{
return
this.MemberwiseClone();
}
}
}
If you elect to return a deep copy,
consider implementing it in terms of a “copy constructor”. A copy constructor is a version of your
class’ constructor that accepts an instance of it’s own type and sets all
internal state of the instance currently being created to be equivalent. It is typically marked private or protected.
Deep Copy Clone Example:
public class SomeClass : ICloneable
{
{
private int _value;
private string _someString;
private Hashtable _ht;
// Generates a new instance of SomeClass the state of
// which is copied from a source SomeClass instance.
private string _someString;
private Hashtable _ht;
// Generates a new instance of SomeClass the state of
// which is copied from a source SomeClass instance.
private
SomeClass(SomeClass rhs)
{
_value = rhs._value;
_someString = rhs._someString;
_ht = (Hashtable)rhs._ht.Clone();
}
{
_value = rhs._value;
_someString = rhs._someString;
_ht = (Hashtable)rhs._ht.Clone();
}
///<summary>
///Returns a new instance that is a deep copy of the current instance
///</summary>
public object Clone()
{
return new
SomeClass(this);
}
}
}
Static Classes
If a class consists only of static methods
and fields, add a private parameterless (default) constructor to ensure a
programmer will not accidentally create an instance of the class (which would
have no practical use.)
In addition, consider marking the class sealed.
Example:
public sealed class FunctionLibrary
{
{
private
FunctionLibrary(){} //”private” ensures
class cannot be instantiated
public static string
DoSomething(...){...}
public static int
DoSomethingElse(...){...}
}
}
Static Methods for Utilities and Factories
Static classes or instance classes with static
methods are a good approach for building utility or factory classes (when
instance state is not important.)
If possible, create a utility class so that
it can be called directly without creating and managing an instance:
double x = 12345;
double log = System.Math.Log10(x);
double log = System.Math.Log10(x);
Instead of:
double x = 12345;
System.Math math = new System.Math();
double log = math.Log10(x); // This is a fictional method, but illustrates the point
double log = math.Log10(x); // This is a fictional method, but illustrates the point
// Alternatively, inlining the instance as:
double log = new System.Math().Log10(x);
// would be shorter, but still requires the wasteful overhead of the instance. Avoid this.
double log = new System.Math().Log10(x);
// would be shorter, but still requires the wasteful overhead of the instance. Avoid this.
Remember, a static class can have state,
but since it is static (class-level)
it has to be meaningful and correct for all callers of that single static
instance and any changes to the static state made outside of a constructor
should be threadsafe as well.
Finalization, Garbage Collection
and Disposing
If (and only if) your class has holds
references to unmanaged resources or, to a lesser extent, must execute cleanup
code before it is garbage collected, you should implement a finalizer. (A finalizer is somewhat similar to what a
destructor is in C++.)
However, given the nondeterministic nature
of the garbage collection system in the CLR, it may be necessary to control the
timing of the execution of your finalization code, so a simple destructor is
often insufficient. In these cases, you
should implement the IDisposable
interface as follows:
public class ClassNeedingDisposal : IDisposable
{
private bool _isDisposed = false; // Flag to prevent double disposal
~ClassNeedingDisposal()
{
Dispose(false); // Call Dispose here in case it was not called manually
// You might consider adding this debug call to warn developers to call Dispose:
//System.Diagnostics.Debug.Assert(false, "Dispose() was not called on an instance of the class \"ClassNeedingDisposal\".");
}
public void Dispose()
{
Dispose(true); // Call Dispose with flag to kill managed resources
GC.SuppressFinalize(this); // Tell CLR not to call ~ClassNeedingDisposal
}
protected virtual void Dispose(bool disposeManaged)
{
if(!_isDisposed)
{
if(disposeManaged)
{
// Dispose any managed resources held by this instance
}
// Dispose any unmanaged resources
// Do NOT call/use any managed references held by this instance here!
_isDisposed = true;
}
}
}
{
private bool _isDisposed = false; // Flag to prevent double disposal
~ClassNeedingDisposal()
{
Dispose(false); // Call Dispose here in case it was not called manually
// You might consider adding this debug call to warn developers to call Dispose:
//System.Diagnostics.Debug.Assert(false, "Dispose() was not called on an instance of the class \"ClassNeedingDisposal\".");
}
public void Dispose()
{
Dispose(true); // Call Dispose with flag to kill managed resources
GC.SuppressFinalize(this); // Tell CLR not to call ~ClassNeedingDisposal
}
protected virtual void Dispose(bool disposeManaged)
{
if(!_isDisposed)
{
if(disposeManaged)
{
// Dispose any managed resources held by this instance
}
// Dispose any unmanaged resources
// Do NOT call/use any managed references held by this instance here!
_isDisposed = true;
}
}
}
NOTE:
Take care to only implement a finalizer or the IDisposable interface when necessary as doing so adds to system
overhead. Most classes do not require finalization.
Why is a Boolean passed to a virtual implementation of Dispose? When a finalizer is called by the runtime,
you can no longer be certain that the managed resources held by the instance
are still available. In this case, you
should not touch nor rely upon reference to managed resources. Doing so may result in what is known as
instance “resurrection” and hard-to-diagnose side effects. False is passed in from the finalizer to tell
Dispose not to touch managed resources.
It is also good practice in a disposable
class to reference the _isDisposed Boolean
in the methods of that class to ensure they are not called after a call to
Dispose(). In such cases, throw a System.ObjectDisposedException.
Example:
public string DoSomething(int someID)
{
{
if(_isDisposed)
{
throw new ObjectDisposedException(“Unable to process – Dispose() has already been called on this instance.”);
}
...
}
throw new ObjectDisposedException(“Unable to process – Dispose() has already been called on this instance.”);
}
...
}
Additionally, a class that implements IDisposable because it holds references
to unmanaged resources should inherit from System.MarshalByRefObject. This is because unmanaged resources cannot be
marshaled by value in Remoting scenarios.
Wrap Disposable Objects
with using
When working with an instance of a class
that implements IDisposable, your
code will be simpler and less error-prone if you wrap the lifetime of that
instance with the using statement. When control passes outside the using block,
the Dispose method of the object is automatically called – even when an Exception has been thrown. Multiple using statements can be
stacked before a code region.
Example:
using (ClassNeedingDisposal cnd = new ClassNeedingDisposal())
using (ClassNeedingDisposal cnd2 = new ClassNeedingDisposal())
{
using (ClassNeedingDisposal cnd2 = new ClassNeedingDisposal())
{
... use cnd and cnd2 variables here
} // cnd.Dispose() and cnd2.Dispose() called automatically on exit (or exception)
During compilation to MSIL, the using construct expands to a try/finally
block.
[For a description of GC and finalization, see
“Pinning and Memory Management” in Inside C#, 2nd Edition by
Archer and Whitechapel.]
Optional Parameters via Method
Overloading
You may be used to the Optional keyword from Visual Basic.
The concept of optional parameters is not directly supported in C#, but
there are ways to simulate them. By
using method overloading, a method can expose numerous variations of its
parameter list and chain them to functionally create default and/or optional
settings.
For example, if you wanted a method to
accept a string, and optionally an integer, you could code the method
signatures as follows:
public string ChainedExample(string a)
{
// Call next version, supplying a default integer value
return ChainedExample(a, SOME_DEFAULT_VALUE);
// Call next version, supplying a default integer value
return ChainedExample(a, SOME_DEFAULT_VALUE);
}
public string ChainedExample(string a, int b)
{
// Do the real processing here, either with a user-provided int b
// or the default one passed by the above ChainedExample overload.
{
// Do the real processing here, either with a user-provided int b
// or the default one passed by the above ChainedExample overload.
return ...
}
}
A developer could call either ChainedExample(“test”) or ChainedExample(“test”, 123). Note also that the return value of the second
method is itself returned to the caller by the first method. Methods overloaded in this fashion are
automatically represented as a list in VS.Net IntelliSense with arrow selectors
and the text “x of y”.
Explicit Interface Implementation
An interface is typically implemented
exposing the members with a public access modifier as follows:
Example – Publicly implemented interface
public class NormalWay : IComparable
{
private int _someID;
{
private int _someID;
public CompareTo(object o)
{
return (_someID – ((NormalWay)o)._someID);
}
}
{
return (_someID – ((NormalWay)o)._someID);
}
}
However, there is an alternate, but
infrequently used, way to implement an interface, called explicit
implementation. This approach
qualifies the implemented interface member with the name of the interface. Additionally, no access modifiers are (or can
be) used:
Example – Explicitly implemented interface
public class ExplicitWay : IComparable
{
private int _someID;
{
private int _someID;
int IComparable.CompareTo(object
o)
{
return (_someID – ((ExplicitWay)o)._someID);
}
}
{
return (_someID – ((ExplicitWay)o)._someID);
}
}
The result of using the second approach is
that the implemented member (in this case CompareTo()) will not appear
at the class level of the implementing class.
In order to see that member, you must cast your object reference to the
interface.
For example, to call CompareTo() on an
instance of the ExplicitWay class, you need to do something like the following:
ExplicitWay ew1 = new ExplicitWay (...);
ExplicitWay ew2 = new ExplicitWay (...);
IComparable ic1 = (IComparable)ew1;
int result = ic1.CompareTo(ew2);
ExplicitWay ew2 = new ExplicitWay (...);
IComparable ic1 = (IComparable)ew1;
int result = ic1.CompareTo(ew2);
Trying “ew1.CompareTo(ew2)”
results in a compilation error because you need an IComparable view, not an
ExplicitWay view, to see it.
So, why do this?
There are two main reasons to consider
using explicit implementation. The first
is name hiding. This is useful for reducing the clutter of the
class implementing an interface. If the
specific members implemented for an interface are not of general use to a
consumer of the class, consider using explicit implementation to hide those
members from the “main view” of the class.
They are of course still very much available, but the consumer must
first cast the reference to the correct interface before using that member.
The second, but more important, reason is
for resolving naming conflicts.
Say a method, Execute(), is required with the same signature by
more than one interface which your class implements. Using explicit implementation allows you to
code specific implementations for each Execute() method appropriate to
each interface:
Example – Using explicit implementation to avoid name
conflicts
public interface IDoSomething
{
void Execute();
}
public interface IDoSomethingElse
{
void Execute();
}
public class ExplicitWay : IDoSomething, IDoSomethingElse
{
void IDoSomething.Execute()
{
Console.WriteLine(“IDoSomething is doing something.”);
return;
}
public class ExplicitWay : IDoSomething, IDoSomethingElse
{
void IDoSomething.Execute()
{
Console.WriteLine(“IDoSomething is doing something.”);
return;
}
void IDoSomethingElse.Execute()
{
Console.WriteLine(“IDoSomethingElse is doing something else.”);
return;
}
}
{
Console.WriteLine(“IDoSomethingElse is doing something else.”);
return;
}
}
The above example would require a consumer
of ExplicitWay to cast the reference to either interface before invoking Execute(). This approach helps to avoid ambiguity.
[For a more detailed discussion, see “Explicit
Interface Member Name Qualification” in Inside C#, 2nd Edition
by Archer and Whitechapel.]
Comments
Post a Comment