Click here to Skip to main content
Click here to Skip to main content

General Guidelines for C# Class Implementation

, 2 Aug 2002 CPOL
Rate this:
Please Sign up or sign in to vote.
Practical tips for making your classes good friends with .NET

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

  1. 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; 
    } 
    
    public static bool operator<(MyType lhs, MyType rhs)
    { 
        return ((IComparable)lhs).CompareTo(rhs) < 0;
    }
    
    public static bool operator<=(MyType lhs, MyType rhs)
    { 
        return ((IComparable)lhs).CompareTo(rhs) <= 0;
    }
    
    public static bool operator>(MyType lhs, MyType rhs)
    { 
        return ((IComparable)lhs).CompareTo(rhs) > 0;
    }
    
    public static bool operator>=(MyType lhs, MyType rhs)
    { 
        return ((IComparable)lhs).CompareTo(rhs) >= 0;
    }
  2. 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;
    }
    
    public static explicit operator MyType(int from)
    { 
        return new MyType(from);
    }
    
    public static implicit operator int(MyType from)
    { 
        return from.m_data;
    }
  3. Always implement Object.ToString() to return a significant textual representation.
    public override string ToString()
    { 
        return string.Format("MyType: {0}", m_data);
    }
  4. 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().
     
    public override int GetHashCode()
    { 
        return m_data.GetHashCode();
    }
    
    public override bool Equals(object obj)
    { 
        // Call base.Equals() only if this class derives from a 
        // class that overrides Equals()
        if(!base.Equals(obj))
           return false;
    
        if(obj == null)
           return false; 
    
        // Make sure the cast that follows won't fail
        if(this.GetType() != obj.GetType())
           return false;
    
        // Call this if m_data is a value type
        MyType rhs = (MyType) obj;
        return m_data.Equals(rhs.m_data);  
    
        // Call this if m_data is a reference type
        //return Object.Equals(m_data, rhs.m_data); 
    }
    
    public static bool operator==(MyType lhs, MyType rhs)
    { 
        if(lhs == null) 
            return false; 
    
        return lhs.Equals(rhs);
    }
    
    public static bool operator!=(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.
    public override int GetHashCode()
    { 
        return m_data.GetHashCode();
    }
    
    public override bool Equals(object obj)
    { 
        if(!(obj is MyType))
            return false;
    
        return this.Equals((MyType) obj);
    }
    
    public bool Equals(MyType rhs)
    {
        // Call this if m_data is a value type
        return m_data.Equals(rhs.m_data);          
    
        // Call this if m_data is a reference type
        //return Object.Equals(m_data, rhs.m_data); 
    }
    
    public static bool operator==(MyType lhs, MyType rhs)
    { 
        return lhs.Equals(rhs);
    }
    
    public static bool operator!=(MyType lhs, MyType rhs)
    { 
        return !lhs.Equals(rhs);
    }
  5. Enumerations that represent bit masks should have the [Flags] attribute.

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

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

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

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

  10. strings 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.

  11. object.MemberWiseClone() provides shallow copying. Implement ICloneable to provide deep copy for classes. ICloneable.Clone() is usually implemented in terms of a copy constructor.
    public MyType(MyType rhs)
    { 
        m_data = rhs.m_data;
    } 
    
    public object Clone()
    { 
        return new MyType(this); 
    }
  12. 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.
    public object this[int index]
    { 
        get { return m_data[index]; } 
        set { m_data[index] = value; }
    }
  13. For a "collection" class to be used in a foreach loop it must implement IEnumerable. IEnumerable.GetEnumerator() returns a class the implements IEnumerator.
    public class MyCollection: IEnumerable
    { 
        public IEnumerator GetEnumerator()
        { 
            return new MyCollectionEnumerator(this);
        }
    }
  14. 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.
    private class MyCollectionEnumerator: IEnumerator
    { 
        public MyCollectionEnumerator(MyCollection col)
        { 
            m_col = col;
            m_lastChanged = col.LastChanged;
        } 
    
        public bool MoveNext()
        { 
            if(m_lastChanged != m_col.LastChanged)
                throw new InvalidOperationException();
    
            if(++m_index >= m_col.Data.Count)
                return false; 
    
            return true;           
        }
    
        public void Reset()
        { 
            if(m_lastChanged != m_col.LastChanged)
                throw new InvalidOperationException();
    
            m_index = -1;
        }
    
        public object Current
        { 
            get { return m_col.Data[m_index]; } 
        } 
    }
  15. 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...
    public class MyClass
    {
        ...
        public ~MyClass()
        {
            Dispose(false);
        }
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);  // Finalization is now unnecessary
        }
       
        protected virtual void Dispose(bool disposing)
        {
            if(!m_disposed)
            {
                if(disposing)
                {
                    // Dispose managed resources
                }
             
                // Dispose unmanaged resources
            }
          
            m_disposed = true;
        }
       
        private bool m_disposed = false;
    }
    
    And use the using statement to dispose resources as soon the object goes out of scope
    using(MyClass c = new MyClass())
    {
        ...
    } // The compiler will call Dispose() on c here
  16. 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.
      public void DivByZero()
      {
          int x = 1 / 0; // Our caller better be ready to deal with this!
      }
    • Catch the exception, cleanup and re-throw
      try
      {
          ...
      }
      catch(Exception ex)
      {
          // do some cleanup
          throw;
      }
    • Catch the exception, add information and re-throw
      try
      {
          ...
      }
      catch(Exception ex)
      {
          throw new Exception("Something really bad happened!", ex);
      }
  17. When catching exceptions, always try to catch the most specific type that you can handle.
    try
    {
        int i = 1 / 0;
    }
    catch(DivideByZeroException ex) // Instead of catch(Exception ex)
    {
        ...
    }
    
  18. If you need to define your own exceptions, derive from System.ApplicationException, not from System.Exception.

  19. Microsoft's FxCop design diagnostic tool is your friend. Use it regularly to validate your assemblies.

  20. The definite guide for .NET class library designers is .NET Framework Design Guidelines.

Updates

04/20/2002 Item 4 did not mention that GetHashCode's result should be immutable during the lifecycle of the object. (Thanks to James T. Johnson)
Item 14 did not mention that if the underlying collection is modified in any way, the enumerator is invalidated and throws an InvalidOperationException on calls to MoveNext and Reset. (Thanks to James T. Johnson)
Item 7 had a typo: instead of "struct" it should be "class". (Thanks to Blake Coverett)
04/23/2002 Added new items about garbage collection and exception handling. (Thanks to Dale Thompson)
04/25/2002 Item 16c has a typo, when rethrowing you should not include the current exception in the throw statement. (Thanks to Blake Coverett)
05/14/2002 Item 4 suggests implementing == and != in terms of Objects.Equals() which is ok for reference types but introduces unnecessary boxing and unboxing for value types. (Thanks to Eric Gunnerson)
06/03/2002 Restructured the code for Equals(), == and != in item 4 to follow a cleaner and more secure implementation. (Thanks to Jeffrey Richter)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Eddie Velasquez
Software Developer (Senior)
United States United States
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalMohammed Hameed6-Jun-13 2:34 
GeneralThe article might have died but... PinmemberFredrik N8-Feb-08 9:21 
GeneralRe: The article might have died but... PinmemberEddie Velasquez20-Feb-08 5:03 
Generaltry ... finally PinmemberRüdiger Klaehn14-Nov-04 10:31 
GeneralRe: try ... finally Pinmemberleppie14-Nov-04 12:02 
GeneralRe: try ... finally PinmemberAndrew Phillips5-May-08 20:16 
GeneralRegarding point 6 PinmemberRichard Poole3-Nov-04 2:03 
GeneralRe: Regarding point 6 PinmemberEddie Velasquez3-Nov-04 2:49 
GeneralException handling PinmemberRichard Poole2-Nov-04 14:12 
GeneralRe: Exception handling Pinmembergoodmast3r2-Nov-04 15:03 
GeneralRe: Exception handling PinmemberEddie Velasquez3-Nov-04 2:53 
GeneralRe: Exception handling PinmemberRichard Poole4-Nov-04 9:18 
GeneralFYI on 2 of your points PinmemberJudah Himango14-Jun-04 10:11 
GeneralRegarding the IDisposable.. suggestion PinsussBlue Tender11-Mar-04 0:07 
GeneralRe: Regarding the IDisposable.. suggestion Pinmemberjdkulkarni13-Mar-07 21:32 
GeneralIndexers on point 12 Pinmembercosh29-Jul-03 8:30 
GeneralRe: Indexers on point 12 PinmemberDaniël Pelsmaeker6-Sep-03 3:55 
GeneralGot my 5 PinmemberPaul Evans23-Jul-03 4:52 
GeneralRe: Got my 5 PinmemberPaul Evans23-Jul-03 4:57 
GeneralRe: Got my 5 PinmemberEddie Velasquez23-Jul-03 6:19 
GeneralRe: Got my 5 PinmemberEddie Velasquez23-Jul-03 6:17 
GeneralRelated to point 9 PinmemberLars Heyden5-Jun-03 14:04 
GeneralRe: Related to point 9 PinmemberEddie Velasquez5-Jun-03 16:46 
GeneralAbout item 5 PinmemberAlvaro Mendez7-Apr-03 11:27 
GeneralRe: About item 5 PinmemberEddie Velasquez7-Apr-03 11:38 
GeneralOperator Overloading PinmemberSimon Armstrong29-Jan-03 0:38 
GeneralRe: Operator Overloading PinmemberEddie Velasquez29-Jan-03 3:19 
GeneralRe: IEnumerator PinmemberKenD9-Dec-02 6:47 
GeneralRe: IEnumerator PinmemberEddie Velasquez9-Dec-02 6:59 
GeneralRe: IEnumerator Pinmembermousedoc29-Sep-09 15:11 
Generaloperator== on reference types PinmemberMaximilian Hänel29-Oct-02 3:10 
GeneralRe: operator== on reference types PinmemberEddie Velasquez29-Oct-02 3:37 
GeneralRe: operator== on reference types PinmemberMaximilian Hänel29-Oct-02 4:36 
GeneralRe: operator== on reference types PinmemberEddie Velasquez29-Oct-02 5:15 
GeneralRe: operator== on reference types PinmemberMaximilian Hänel29-Oct-02 6:00 
GeneralRe: operator== on reference types PinmemberEddie Velasquez29-Oct-02 6:37 
GeneralRe: operator== on reference types PinmemberMaximilian Hänel29-Oct-02 6:53 
QuestionApplies to C++, VB.NET etc. classes, no? PinsitebuilderPaul Watson4-Aug-02 5:00 
AnswerRe: Applies to C++, VB.NET etc. classes, no? PinmemberEddie Velasquez4-Aug-02 5:17 
GeneralCLS compliance PinmemberNemanja Trifunovic3-Aug-02 16:31 
GeneralRe: CLS compliance PinmemberEddie Velasquez4-Aug-02 5:20 
GeneralRe: CLS compliance PinmembertheRealCondor24-Oct-02 10:44 
GeneralRe: CLS compliance PinmemberEddie Velasquez24-Oct-02 10:57 
GeneralRe: CLS compliance Pinmemberdacris29-May-03 11:12 
GeneralRe: CLS compliance PinmemberEddie Velasquez29-May-03 11:31 
General#13 is incorrect PinmemberTom Archer8-Jun-02 19:56 
GeneralRe: #13 is incorrect PinmemberEddie Velasquez9-Jun-02 5:03 
GeneralRe: #13 is incorrect PinmemberTom Archer9-Jun-02 5:28 
GeneralRe: #13 is incorrect PinmemberEddie Velasquez9-Jun-02 5:53 
GeneralRe: #13 is incorrect Pinmemberpatnsnaudy14-Apr-03 12:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141216.1 | Last Updated 3 Aug 2002
Article Copyright 2002 by Eddie Velasquez
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid