Introduction
While implementing my first projects using C# I found out that there were several issues to take into account if I wanted my classes to behave correctly and make good friends with .NET. This list is more about the "hows" and the "whats" and not the "whys" and while it is in no way complete, it contains the guidelines that I currently follow. Ah, and I almost forgot... it's guaranteed to be incomplete!
The Guidelines
- Implement
IComparable.CompareTo()
if ordering of objects makes sense. Also implement operators <
, <=
, >
and >=
in terms of IComparable.CompareTo()
. int IComparable.CompareTo(object obj)
{
return m_data - ((MyType)obj).m_data;
}
publicstaticbooloperator<(MyType lhs, MyType rhs)
{
return ((IComparable)lhs).CompareTo(rhs) < 0;
}
publicstaticbooloperator<=(MyType lhs, MyType rhs)
{
return ((IComparable)lhs).CompareTo(rhs) <= 0;
}
publicstaticbooloperator>(MyType lhs, MyType rhs)
{
return ((IComparable)lhs).CompareTo(rhs) > 0;
}
publicstaticbooloperator>=(MyType lhs, MyType rhs)
{
return ((IComparable)lhs).CompareTo(rhs) >= 0;
}
- Implement conversion functions only if they actually make sense. Make conversions explicit if the conversion might fail (throw an exception) or if the conversion shouldn’t be abused and it’s only necessary for low level code. Only make conversions implicit if it makes the code clear and easier to use and the conversion cannot fail.
private MyType(int data)
{
m_data = data;
}
publicstaticexplicitoperatorMyType(int from)
{
returnnew MyType(from);
}
publicstaticimplicitoperatorint(MyType from)
{
return from.m_data;
}
- Always implement
Object.ToString()
to return a significant textual representation. publicoverridestring ToString()
{
returnstring.Format("MyType: {0}", m_data);
}
- Implement
Object.GetHashCode()
and Object.Equals()
if object equality makes sense. If two objects are equal (Object.Equals()
returns true) they should return the same hash code and this value should be immutable during the whole lifecycle of the object. The primary key is usually a good hash code for database objects.
For reference types implement operators ==
and !=
in terms of Object.Equals()
.publicoverrideintGetHashCode()
{
return m_data.GetHashCode();
}
publicoverrideboolEquals(object obj)
{
if(!base.Equals(obj))
returnfalse;
if(obj == null)
returnfalse;
if(this.GetType() != obj.GetType())
returnfalse;
MyType rhs = (MyType) obj;
return m_data.Equals(rhs.m_data);
}
publicstaticbooloperator==(MyType lhs, MyType rhs)
{
if(lhs == null)
returnfalse;
return lhs.Equals(rhs);
}
publicstaticbooloperator!=(MyType lhs, MyType rhs)
{
return !(lhs == rhs);
}
- For value types, implement
Object.Equals()
in terms of a type-safe version of Equals()
to avoid unnecessary boxing and unboxing. publicoverrideintGetHashCode()
{
return m_data.GetHashCode();
}
publicoverrideboolEquals(object obj)
{
if(!(obj is MyType))
returnfalse;
returnthis.Equals((MyType) obj);
}
publicboolEquals(MyType rhs)
{
return m_data.Equals(rhs.m_data);
}
publicstaticbooloperator==(MyType lhs, MyType rhs)
{
return lhs.Equals(rhs);
}
publicstaticbooloperator!=(MyType lhs, MyType rhs)
{
return !lhs.Equals(rhs);
}
- Enumerations that represent bit masks should have the
[Flags]
attribute.
- All classes and public members should be documented using XML comments. Private members should be documented using normal comments. XML comments should at least include
<summary>
, <param>
and <returns>
elements.
- If a class is just meant to be a "container" for static methods (has no state), it should declare a private parameter-less constructor so it can’t be instantiated.
- All classes should be CLS compliant. Add an
[assembly:CLSCompliant(true)]
attribute in the AssemblyInfo.cs file. If it is convenient to add a non-CLS compliant public member add a [CLSCompliant(false)]
attribute to it.
- All implementation details should be declared as
private
members. If other classes in the same assembly need access, then declare them as internal
members. Try to expose as little as possible without sacrificing usability.
-
string
s are immutable objects and always create a new copy for all the mutating operations, which makes it inefficient for assembling strings. StringBuilder
is a better choice for this task.
-
object.MemberWiseClone()
provides shallow copying. Implement ICloneable
to provide deep copy for classes. ICloneable.Clone()
is usually implemented in terms of a copy constructor. publicMyType(MyType rhs)
{
m_data = rhs.m_data;
}
publicobjectClone()
{
returnnew MyType(this); //调用拷贝构造函数
}
- If a class represents a collection of objects implement one or more indexers. Indexers are a special kind of property so they can be read-only, write-only or read-write.
publicobjectthis[int index]
{
get { return m_data[index]; }set { m_data[index] = value; }
}
- For a "collection" class to be used in a
foreach
loop it must implementIEnumerable
. IEnumerable.GetEnumerator()
returns a class the implements IEnumerator
. publicclass MyCollection: IEnumerable
{
public IEnumerator GetEnumerator()
{
returnnewMyCollectionEnumerator(this);
}
}
- The
IEnumerator
for a "collection" class is usually implemented in a private class. An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying or deleting elements, the enumerator is irrecoverably invalidated and the next call to MoveNext
or Reset
throws an InvalidOperationException
. If the collection is modified between MoveNext
and Current
, Current
will return the element that it is set to, even if the enumerator is already invalidated. privateclass MyCollectionEnumerator: IEnumerator
{
public MyCollectionEnumerator(MyCollection col)
{
m_col = col;
m_lastChanged = col.LastChanged;
}
publicboolMoveNext()
{
if(m_lastChanged != m_col.LastChanged)
thrownew InvalidOperationException();
if(++m_index >= m_col.Data.Count)
returnfalse;
returntrue;
}
publicvoidReset()
{
if(m_lastChanged != m_col.LastChanged)
thrownew InvalidOperationException();
m_index = -1;
}
publicobjectCurrent
{
get { return m_col.Data[m_index]; }
}
}
- There is no deterministic destruction in C# (gasp!), which means that the Garbage Collector will eventually destroy the unused objects. When this scenario is not ok, implement
IDisposable
... publicclass MyClass
{
...
public ~MyClass()
{
Dispose(false);
}
publicvoid Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protectedvirtualvoid Dispose(bool disposing)
{
if(!m_disposed)
{
if(disposing)
{
}
}
m_disposed = true;
}
privatebool m_disposed = false;
}
And use the using
statement to dispose resources as soon the object goes out of scope using(MyClass c = new MyClass())
{
...
}
- There is no way to avoid exceptions handling in .NET. There are several strategies when dealing with exceptions:
- Catch the exception and absorb it
try
{
...
}
catch(Exception ex)
{
Console.WriteLine("Opps! Something failed: {0}", ex.Message);
}
- Ignore the exception and let the caller deal with it if there's no reasonable thing to do.
publicvoid DivByZero()
{
int x = 1 / 0;
}
- Catch the exception, cleanup and re-throw
try
{
...
}
catch(Exception ex)
{
throw;
}
- Catch the exception, add information and re-throw
try
{
...
}
catch(Exception ex)
{
thrownew Exception("Something really bad happened!", ex);
}
- When catching exceptions, always try to catch the most specific type that you can handle.
try
{
int i = 1 / 0;
}
catch(DivideByZeroException ex)
{
...
}
- If you need to define your own exceptions, derive from
System.ApplicationException
, not from System.Exception
.
- Microsoft's FxCop design diagnostic tool is your friend. Use it regularly to validate your assemblies.
- The definite guide for .NET class library designers is .NET Framework Design Guidelines .