The Study

web-home of chrisatthestudy

github twitter

This website includes: No ads - No cookies - No tracking

Programming - C#

These are my notes from learning C#, collected into a single page.

Compiling from the command-line

First, make sure the path is set up correctly. In a command-prompt window, run the following:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\vsvars32.bat

01 - Basic command-line application

// hello.cs
// arguments: A B C D
using System;

namespace RCS
{
    public class Program
    {
        // Mandatory static Main method, the entry-point to the application
        public static int Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
            Console.WriteLine("Found {0} command line arguments:",
               args.Length );
            for (int i=0; i < args.Length; i++)
            {
                Console.WriteLine("{0}", args[i]);
            }
            return 0;
        }
    }
}

02 - Namespaces and Assembly References

Nested namespaces can be collapsed using the . syntax:

namespace Outer.Inner { }

is the same as:

namespace Outer {
    namespace Inner {

    }
}

Using

Specifies namespaces required by a unit:

using System.Console

It allows for aliases:

using Output = System.Console;
class Hello
{
    public static void Main()
    {
        Output.WriteLine("Hello");
    }
}

External Aliases

To avoid a name conflict in an external assembly, an alias can be supplied to the compiler, either via a /reference:Alias = full.assembly.name.dll on the command-line, or using the Alias property in the Visual Studio reference properties.

Assemblies

By default the C# compiler only automatically imports mscorlib.dll -- any other assemblies must be explicitly specified.

For the command-line compiler use the /r: switch:

csc ProgramUsingSystemNet /r:System.Net.dll

03 - Basic Data Types (Common Type System)

Type Size CTS Type Description
byte 1 System.Byte Unsigned byte
sbyte 1 System.SByte Signed byte
short 2 System.Int16 Signed short
ushort 2 System.UInt16 Unsigned short
int 4 System.Int32 Signed integer
uint 4 System.UInt32 Unsigned int
long 8 System.Int64 Signed big integer
ulong 8 System.UInt64 Unsigned big integer
float 4 System.Single Floating point number
double 8 System.Double Double-precision floating point number
decimal 8 System.Decimal Fixed-precision number
char 2 System.Char Unicode character
bool 1 System.Boolean Boolean value
string - System.String Unicode string
object - System.Object Root type

Note that string and object are reference types. All the others are value types.

Also note that strings do not act like normal reference types, because strings are immutable -- if you change a string, a completely new string is created. This means that after the following lines, s1 and s2 hold different values:

string s1 = "First string";
string s2 = s1;

s1 = "Modified string";

At this point, s2 still holds "First string".

04 - ref and out Parameters

These are akin to Delphi 'var' parameters. The main difference between them is that ref does not accept uninitialised parameters, whereas out does. There is an implied difference in the way that they are expected to be used: ref parameters are for parameters which are passed in and modified in some way, whereas out is for parameters which are simply used to return values from the method.

Note: In CLR they are identical.

Slightly bizarrely the out (or ref) keywords have to be supplied when calling the method as well as in the declaration:

int x;
int y;
Point p = new Point;
p.GetPoint(out x, out y);

Value Types and Reference Types

Value types are allocated on the stack, and assigning a value from one variable type to another creates a copy of the value.

Reference types are allocated on the heap (and must be created using 'new'), and copying from one reference variable to another creates two references to the same value.

05 - Overloading

Overloading of methods is done simply by declaring identically-named methods with differing signatures (there is no 'overload' keyword, or any other way of indicating that the methods are overloaded).

// Overloaded Add() method.
static int Add(int x, int y) { return x + y; }
static double Add(double x, double y) { return x + y; }
static long Add(long x, long y) { return x + y; }

06 - Exceptions

Easiest to demonstrate by example. Note that you can have multiple catch blocks.

using System;
class Test
{
    static int Zero = 0;
    static void AFunction()
    {
        try
        {
            int j = 22 / Zero;
        }
        catch (DivideByZeroException e)
        {
            Console.WriteLine("DivideByZero {0}", e);
        }
        catch (Exception e)
        {
            throw(new Exception("An unanticipated exception occurred in AFunction()", e));
            // You could instead do other handling here and then simply re-throw:
            //
            //     Console.WriteLine("Something bad happened!");
            //     throw;
        }
    }
    public static void Main()
    {
        try
        {
            AFunction();
        }
        catch (Exception e)
        {
            Console.WriteLine("Unexpected exception {0}", e);
        }
    }
}

.NET has a top-level handler system for exceptions, and allows application-specific handlers to be added to it:

static void Main(string[] args)
{
    AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;
    // ... other code ...
}
static void UnhandledExceptionHandler (object sender, UnhandledExceptionEventArgs e)
{
    Exception exception = (Exception) e.ExceptionObject;
    Console.WriteLine(exception);
    System.Diagnostics.Debugger.Break();
}

When you throw a new exception from within a catch handler, this wraps the original exception in a new exception. The chain of exceptions can be accessed using the innerException property of the Exception class, which contains the Exception that was originally caught.

Generally a new Exception should be thrown if it can be used to add details about the actual exception that occurred.

When re-throwing the same Exception, use 'throw', not 'throw e', i.e. do not explicitly throw the caught Exception, as this will actually create a new Exception stack and discard the original one, losing the information.

Exception Members

Example

ex = new Exception(string.Format("Error in {0}", "");
ex.HelpLink = "http://www.gethelphere.com";
ex.Data.Add("Problem", "Something went wrong");
ex.Data.Add("TimeStamp", string.Format("It happened at {0}", DateTime.Now));
throw ex;


You can check if an error is actually from the .NET runtime:

catch (Exception e)
{
    if e is SystemException
        Console.WriteLine(".NET Runtime error: {0}", e.Message);
}

07 - Custom Exceptions

Custom exceptions should inherit from Exception. Exceptions are perfectly ordinary classes.

From Microsoft MSDN: "For most applications, derive custom exceptions from the Exception class. It was originally thought that custom exceptions should derive from the ApplicationException class; however in practice this has not been found to add significant value."

Example

This conforms to Microsoft's "best practices" for Exception classes. Visual Studio includes a code snippet ('exception') that can help defining these.

[Serializable]
public class ExampleException : Exception
{
    public DateTime ErrorTimeStamp { get; set; }
    public string CauseOfError { get; set; }

    // Best practice: define a default constructor
    public ExampleException() { }

    // Best practice: define a constructor that takes the message param
    public ExampleException(string message): base(message) { }

    // Best practice: define a constructor to handle "inner exceptions"
    public ExampleException(string message, SystemException inner)
           : base(message, inner) { }

    // Best practice: protected constructor for inner exceptions
    protected ExampleException(
              System.Runtime.Serialization.SerializationInfo info,
              System.Runtime.Serialization.StreamingContext context)
          : base(info, context) { }

    // Pass message to parent constructor.
    public ExampleException(string message, string cause, DateTime time)
    :base(message)
    {
        CauseOfError = cause;
        ErrorTimeStamp = time;
    }
}

08 - Inner Exceptions

If an exception happens within a 'catch' block it should be caught, and a new exception raised as a inner exception (hence the inner exception constructor in the above code):

catch(Exception e)
{
    try
    {
        // Try to handle the exception...
    }
    catch (Exception inner)
    {
        // Something went wrong, so pass the current error message and
        // the inner exception to another exception
        throw new ExampleException(e.Message, inner);
    }
}

09 - Access: internal

Classes can be public, protected, or internal. Classes marked as internal are only accessible to other classes in the same assembly.

The same specifier can be used on member variables, with the same result that they are then accessible to other classes in the same assembly.

It also possible to have a protected internal field, which is accessible to descendant classes and to other classes in the same assembly.

Types (e.g. classes) are implicitly internal. Member fields are implicitly private.

Classes can only be public or internal.

10 - Variable-Length Parameter Lists

Like so:

public void Write(string label, params object[] args)
{
    WriteString(label);
    foreach (object o in args)
    {
        WriteString(o.ToString());
    }
}

Write("Label:", "One", 2, "Three");

It's worth noting that a parameter type of object will accept anything passed as it, as it is the lowest-level type.

11 - Optional Parameters

C# methods or functions can take optional parameters, declared in the expected way:

static void EnterLogData(string message, string owner = "Programmer")

12 - Named Arguments

The calling code can identify a parameter by name instead of position, which can sometimes help for clarity.

Assume the following method exists:

public static void LogMessage(string message, bool includeDateAndTime)

It would normally be called like this:

log.LogMessage("Message", true);

However, the argument name can be used, which makes the meaning of 'true' clearer:

log.LogMessage("Message", includeDateAndTime: true);

Also, using optional parameters with named parameters allows a function to be called with only those parameters that want something other than the default.

Note that once you have used a named parameter in a function call, any following parameters must also be named.

13 - Nesting

Classes, interfaces, structs, delegates, and enums can all be nested inside a class.

14 - Inherited Classes

Simply declared:

class Original
{
}

class Descendant: Original
{
}

No multiple inheritance -- classes must descend from a single ancestor.

The sealed keyword prevent inheritance:

sealed class NoInheritanceAllowed
{
}

Note that C# structures are implicitly sealed.

15 - Initialising a Base Constructor

Assuming that the ancestor class has a constructor which takes the listed set of parameters:

public Manager(string fullName, int age, int empID,
           float currPay, string ssn, int numbOfOpts)
       : base(fullName, age, empID, currPay, ssn)
{
    // This property is defined by the Manager class.
    StockOptions = numbOfOpts;
}

16 - Composition

Instances of other classes can be created at the declaration level:

public partial class Employee
{
    // Contain a BenefitPackage object.
    protected BenefitPackage empBenefits = new BenefitPackage();

    // Expose certain benefit behaviors of object.
    public double GetBenefitCost() { return empBenefits.ComputePayDeduction(); }

    // Expose object through a custom property.
    public BenefitPackage Benefits
    {
        get { return empBenefits; }
        set { empBenefits = value; }
    }
}

17 - Nested Classes

As well as structs, enums, constants, member fields, etc., a class can contain other classes.

public class OuterClass
{
// A public nested type can be used by anybody.
public class PublicInnerClass {}
// A private nested type can only be used by members
// of the containing class.
private class PrivateInnerClass {}
}

* Nested types allow control over the access level of the inner type.

  • Nested types can access private members of the containing class.
  • Nested types are often only useful as helpers for the containing class

18 - Abstract Classes

These cannot be instantiated, only inherited from.

abstract class Base { }

Abstract methods must be implemented by descendant classes (and note that the 'abstract' keyword replaces the 'virtual' keyword, which is not used in this case).

public abstract void MustImplement();

Only abstract classes can have abstract methods.

19 - Virtual Methods

As with Delphi, virtual methods must be marked as such, and overriding methods must likewise be indicated:

class Ancestor
{
    public virtual void GiveBonus(float amount) { ... }
}

class Descendant: Ancestor
{
    public override void GiveBonus(float amount)
    {
        // The base method can be called like this
        base.GiveBonus(amount);
        // ... other processing here ...
    };
}

class FinalDescendant: Descendant
{
    // Declaring the method as sealed prevents further overriding
    public override sealed void GiveBonus(float amount) { ... }
}

If a method is not defined as virtual it is still possible to override it in a descendant class by using the 'new' keyword. This makes the method a complete replacement for the ancestor method, which is then completely hidden from the descendant class.

public new void Draw() { ... }

20 - Casting

It is always possible to store a derived class instance in a base class reference:

class Base
{
    public void MethodInBase() { ... }
}

class Derived: Base
{
    public void MethodInDerived() { ... }
}

// Implicit case of Derived as Base
Base base = new Derived()

// The Base methods can be called directly
base.MethodInBase();

// To access the Derived methods a cast is required
(Derived)base.MethodInDerived();

Casts are evaluated at runtime, and raise exceptions if they are invalid. The 'as' keyword can be used to check a cast:

Derived derived = base as Derived;
if (derived == null)
    Console.WriteLine("Not a Derived instance!");
else
    derived.MethodInDerived();

The 'is' keyword can also be used:

if (base is Derived)
    (Derived)base.MethodInDerived();
else
    Console.WriteLine("Not a Derived instance!");

21 - Constructors

If no constructor is specified, there is a default constructor which has no parameters.

Constructors can be overloaded. As soon as a constructor with a non-empty signature is created, the default constructor is no longer available, and one must be explicitly supplied if it will be required.

One constructor can call another in the same class, using 'this':

public MyObject()
{
    this.x = 0;
    this.y = 0;
}
public MyObject(int x)
{
    this.x = x;
}
public MyObject(int x, int y): this(x)
{
    this.y = y;
}

Constructors can call inherited constructors in a similar way, but using 'base' instead of 'this':

class Original
{
    int m_x;
    public Original(int x)
    {
        m_x = x;
    }
}
class Descendant: Original
{
    int m_y;
    public Descendant(int x, int y): base(x)
    {
        m_y = y;
    }
}

22 - Object Initialisation

Properties can be initialised when calling a constructor:

MyObject obj = new MyObject() { x = 10, y = 20 };

The parentheses can be omitted:

MyObject obj = new MyObject { x = 10, y = 20 };

23 - Private Constructors

It's possible to specify a class constructor as being private, in which case it cannot be called from outside the class. In this scenario, it is not possible to instantiate the class, but static functions in it can still be accessed.

This is used for classes which are nothing more than a collection of static functions, and for creating singletons (the singleton class exposes a static function which returns the single, internally-maintained instance of the class).

24 - Initialisation of Member Fields

When declaring member fields for classes these can be initialised at the same time:

class Point
{
    int m_x = 0;
    int m_y = 0;
    // ...
}

25 - Destructors

C# doesn't have any. It has finalizers, which are called when the object is destroyed by the garbage collector and can be used if the class holds resources that need to be cleaned up.

26 - Static Members

Static fields, constructors, and methods are associated with the class rather than with the instance. They can be called/accessed without an instance of the class being available, using the class itself instead:

class StaticTest
{
    public static int instanceCount;
    public StaticTest()
    {
        instanceCount++;
    }
}

class Test
{
    public static void Main()
    {
        st1 = new StaticTest();
        print StaticTest.instanceCount;
        st2 = new StaticTest();
        print StaticTest.instanceCount;
    }
}

Note that unlike some other languages C# considers it an error to attempt to access a static field (or other static object) through an instance rather than through the class.

Static constructors are possible and will be called at some indeterminate point before the first instance of the class is created.

27 - Static Classes

Static classes are essentially nothing more than containers for related static methods. Specifying a class as static prevents instance methods from being added to it.

28 - Static Constructors

If defined, these are called before the first time the class is instantiated, and are never called again for the lifetime of the running application. They are used to safely set static member field values if these need to be set at runtime rather than compile-time.

A static constructor cannot take any parameters.

29 - Constants

Constants are declared with the const keyword. For obvious reasons they must be initialised at the same time, and can only be initialised with literals. Constants are also static.

class Test
{
    public const double  APPROX_PI = 3.141;
}

30 - Read-Only Fields

The restriction on constants that they can only be initialised with literals can be avoided by using a read-only field instead. These fields are initialised in a static constructor, and cannot subsequently be changed.

class Test
{
    public readonly int APPROX_PI;

    public static void Test()
    {
        APPROX_PI = 22 / 7;
    }
}

Read-only fields can be static, in which case they must be assigned in a static constructor.

31 - Partial Classes

These allow classes to be defined across more than one file. A common use is with generated code, where the generated code is in one file, and the code supplied by the programmer is in a separate file.

All the parts of the class must include the 'partial' keyword.

partial class Test
{
    public void generated_method()
    {
        // ...
    }
}

partial class Test
{
    public void handcrafted_method()
    {
        // ...
    }
}

In the above example the classes would usually be in different files.

It also possible to have partial methods. Essentially the generated code includes a declaration but no body. The other part of the class can include the method with a body. If it does not do so, it is assumed that the method is not required, and the compiler will remove it.

Partial methods cannot return values (they must be void and have no out parameters).

32 - Properties

Properties encapsulate getters and setters in a simple format:

private string empName;

public string Name
{
    get { return empName; }
    set
    {
        if (value.Length > 15)
            Console.WriteLine("Error! Name must be less than 16 characters!");
        else
            empName = value;
    }
}

Properties can optionally only define a get or a set (for read-only or write-only versions). They can also be static.

For very simple implementations a short-hand version can be used (these are called automatic properties):

public int Speed { get; set; }

Automatic properties must include both get and set. The values behind them will be set to the default values for the type (note that reference values will default to null).

Note that the value behind an automatic property is not readily accessible without code trickery. If either the get or set needs to be changed from the simple version defined by the automatic property you should implement a full getter and setter, using your own private field for the property.

33 - Interfaces

Interfaces specify the methods that classes must implement, and are rather like abstract classes except that you can inherit from multiple interfaces, and they do not contain any implementation code themselves.

Like Object Pascal the 'is' and 'as' operators are supported, and they work in the same way. 'as' will return null if the interface is not supported.

Methods which take an interface parameter can accept any class instance that supports the specified interface:

public void SomeMethod(ISomeInterface interfacedObject) { ... }

Note that passing an object which does not support the interface will be caught at compile-time.

Interfaces can be return values from method functions.

Arrays of interfaces are perfectly viable (and can contain any objects which support the required interface).

IExample[] ExampleObject = { new ExampleOne(), new OtherObject() };

Interfaces support 'as' and 'is'.

Incorrectly type-casting an object to an unsupported interface raises an InvalidCastException.

34 - Explicit Interface Implementation

If a class uses multiple interfaces, and these interfaces include duplicated methods (which is perfectly acceptable), it is possible to implement separate methods for each one if required:

class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter
{
    // Explicitly bind Draw() implementations
    // to a given interface.
    void IDrawToForm.Draw()
    {
        Console.WriteLine("Drawing to form...");
    }
    void IDrawToMemory.Draw()
    {
        Console.WriteLine("Drawing to memory...");
    }
    void IDrawToPrinter.Draw()
    {
        Console.WriteLine("Drawing to a printer...");
    }
}

However, any explicit interface implementation is automatically private, and the method cannot be called in its simple form. Instead, any call must typecast it to the required interface first:

oct = new Octagon();
((IDrawToForm)oct).Draw();

// Or:

(oct as IDrawToForm).Draw();

It is still acceptable to implement a single method (a Draw() method for the above example) if the required behaviour for each interface method is the same.

Interfaces can extend other interfaces, in the same manner (and using the same declaration syntax) that classes can inherit from other classes:

public interface DescendantInterface: BaseInterface { }

Interfaces can extend multiple interfaces.

As with classes, explicit interface implementation can be used if different implementations of duplicate methods are required.

35 - switch Statement

Every block must have a break or a goto (another case). C# allows string identifiers for the cases (yes!).

switch(identifier)
{
  case "A":
  case "B:
     DoStuff();
     break;
  case "C":
     DoOtherStuff();
     goto case "X";
  case "M":
     SomethingElse();
     break;
  case "X":
     More();
     break;
  default:
     LastChance();
     break;
}

36 - goto Statement

As in the switch statement above, goto can be used to jump to a label. In cases other than the switch statement, the label is a name followed by a colon.

 if something_is_wrong {
   goto exitline;
 } else {
   do_normal_processing();
 }
 exitline:
   return -1;

37 - while Statement

Completely standard:

  while (expr = true) { DoSomething(); }

It supports break and continue.

There is also a do while:

  do { Something(); } while (expr = true);

38 - for Statement

Completely standard:

  for (i = 0; i < 10; i++) { DoSomething(i); }

39 - foreach Statement

Used to iterate over any enumerable collection, using the standard syntax:

  foreach(string Name in someMap.Keys) { Console.Write(Name); }

Note that you cannot change the value of the enumerated item (in this case, you cannot assign a new value to Name).

40 - Enums

enum NumType
{
Zero,
One,
Two,
Three,
Hundred = 100,
Thousand = 1000
}

The underlying type can be specified (it defaults to System.Int32):

enum NumType: byte
{
Zero,
One,
Two,
Max = 255
}

The full type name must be specified when referencing an enum:

NumType valueTwo = NumType.Two;

41 - Arrays

Simple declaration:

   int[] = new int[5];

Arrays can be initialised when they are declared:

   string[] strArray = new string[3] { "One", "Two", "Three" };

The number of elements does not need to be declared:

   string[] strArray = new string[] { "One", "Two", "Three" };

In fact, the entire 'new' segment can be omitted:

   string[] strArray = { "One", "Two", "Three" };

42 - Multidimensional Arrays

These can be defined as rectangular arrays or as jagged arrays. In rectangular arrays the number of elements in each subarray is always the same:

   int[,] rectArray;
   rectArray = new int[6, 10];

Jagged arrays are essentially arrays of subarrays, where each subarray is created separately and can be of a different length.

   int[][] jaggedArray = new int[5][];

   for (int i = 0; i < jaggedArray.Length; i++)
   {
       jaggedArray[i] = new int[i + 5];
   }

Arrays can be method parameters and can be return values. In both cases the size of the array does not need to be declared.

43 - Scope

C# does not allow variables to be redeclared in an inner scope. The following won't compile:

  void Test()
  {
      int x = 10;
      for (i = 0; i < 10; i++)
      {
          int x = 5;
          Console.Write("Test: {0}", x + i);
      }
  }

44 - Initialisation

Structs and classes have their fields initialised to null values. Other data types will raise a compiler error if they remain uninitialised.

45 - Nullable Values

C# supports the concept of value types which can hold a null value as well as the usual values. Reference types can always have a null value.

Nullable types are indicated by a ? suffix:

    bool?[] nullableBools = { true, false, null }

46 - Null Coalescing Operator

Allows a default value to be supplied when assigning a variable:

string test = name ?? "";

47 - checked and unchecked

These allow for overflow exceptions to be raised or ignored. The default (curiously) is unchecked -- i.e. overflow exceptions are ignored and do not raise an error.

They can also be applied to single statements.

uint value1 = 312;
byte value2;

value2 = unchecked((byte) value1);

48 - Enumerable Interfaces

The foreach operator can be applied to any class which supports the IEnumerable interface. This interface has a single method, GetEnumerator, which must return an System.Collections.IEnumerator interface.

The standard array type supports the IEnumerable interface. Any class which wants to expose the elements of the array for external use can simply use the array's GetEnumerator() method:

public IEnumerator GetEnumerator()
{
    // Return the array object's IEnumerator.
    return innerArray.GetEnumerator();
}

The method can be obscured using explicit interface implementation to avoid its use by anything other than foreach (which will look for this specific implementation):

IEnumerator IEnumerable.GetEnumerator()
{
    // Return the array object's IEnumerator.
    return innerArray.GetEnumerator();
}

49 - Iterator Methods

An alternative way to support the IEnumerable interface is to implement a GetEnumerator() method which acts an as iterator.

public IEnumerator GetEnumerator()
{
    foreach (System.Object item in innerArray)
    {
        yield return item;
    }
}

The following is also valid, though weird, and illustrates the point that each call to the method gets the results of the next 'yield' statement:

public IEnumerator GetEnumerator()
{
    yield return innerArray[0];
    yield return innerArray[1];
    yield return innerArray[2];
    yield return innerArray[3];
}

Other methods can also use the 'yield' keyword and will act in the same way, except that they must return an IEnumerable interface (not IEnumerator). As with GetEnumerator() they will return the result of the next 'yield' statment with each call.

public IEnumerable GetItems(bool reversed)
{
    if (reversed)
    {
        for (int i = innerArray.Length; i != 0; i--)
        {
            yield return innerArray[i-1];
        }
    }
    else
    {
        foreach(System.Object item in innerArray)
        {
            yield return item;
            }
    }
}

50 - ICloneable Interface

The ICloneable interface has a single method, Clone() which returns an object that is a copy of the object it is invoked on. Because it returns a basic object, the result must be typecast to the required type.

public class Example: ICloneable
{
    // ...
    public object Clone()
    {
        return new Example();
    }
}

There is a MemberwiseClone() method which can be used to simplify this:

public object Clone()
{
    return MemberwiseClone();
}

This only performs a shallow copy. To correctly copy any member fields which are class instances, the method must be extended:

public object Clone()
{
    // Use MemberwiseClone() to get the basic copy
    Example clone = (Example)this.MemberwiseClone();

    // Now create any contained objects
    InteriorClass inner = new InteriorClass();

    // Copy any details from the current instance
    inner.field = this.inner.field;

    // Assign the contained object to the clone
    clone.inner = inner;

    // Return the final copy
    return clone;
}

51 - IComparable and IComparer

The IComparable interface has a single method, CompareTo(), which takes an object as a parameter. It should return 1 if the current object should be regarded as 'greater' than the supplied object, -1 if the current object should be regarded as 'less' than the supplied object, and 0 if they are at the same level.

public class Example: IComparable
{
    int id;

        // ...

        int IComparable.CompareTo(object o)
    {
       Example temp = o as Example;
       if (temp != null)
       {
          if (this.id > temp.id)
              return 1;
          if (this.id < temp.id)
              return -1;
              else
              return 0;
       }
       else
           throw new ArgumentException("Supplied object is not an Example");
    }
}

Note that the primitive types (such as int) support the IComparable interface, so the comparison above could be simplified:

if (temp != null)
    return this.id.CompareTo(temp.id);
else
    throw new ArgumentException("Supplied object is not an Example");

52 - IComparer

Helper classes implementing the IComparer interface can be used to allow sorting by different parameters, creating a new class for each sort type required.

These implement a Compare(object, object) method which compares the two supplied objects and returns the same values as returned by CompareTo.

public class ExampleNameComparer: IComparer
{
    public static int Compare(object o1, object o2)
    {
        Example t1 = o1 as Example;
        Example t2 = o2 as Example;
        if ((t1 != null) && (t2 != null))
        {
        return String.Compare(t1.Name, t2.Name);
        }
        else
        throw new ArgumentException("Invalid object type supplied");
    }
}

The array type has a version of Sort which takes an object implementing the IComparer interface:

Array.Sort(someArray, new ExampleNameComparer());

As a better alternative, the main class can implement a static method which returns the appropriate comparer class:

public class Example: IComparable
{
    public static IComparer SortByName
    {
        get { return (IComparer)new ExampleNameComparer(); }
    }
}

Array.Sort(arrayOfExamples, Example.SortByName);

53 - Collections

.NET includes a set of non-generic collections, but these have generally been superseded by generic versions. The non-generic collections invariably store objects and have to be type-cast back to the required type when they are read. For value-types the values are actually boxed and unboxed -- boxing is done automatically when the values are added to the collection, and unboxing occurs when the values are type-cast (on reading them from the collection).

The resulting performance issues and lack of type-safety are addressed by the generic versions of these collections.

Generics allow the type to be specified when the collection is created, enforcing type-safety, and avoiding the necessity to box and unbox the values:

List intList = new List();
intList.Add(10);

List exampleList = new List();
exampleList.Add(new Example());

There are also generic versions of some class member methods:

int[] intArray = { 1, 5, 6, 2, 4, 3 }

Array.Sort(intArray);
foreach(int i in intArray)
{
    Console.WriteLine(i);
}

Generics can simplify classes which use interfaces like IComparable. For example, compare this with the example given under IComparable and IComparer above:

public class Example: IComparable
{
int id;

    // ...

    // The actual class type can be specified
        int IComparable.CompareTo(Example o)
    {
        // No need to check that we have the right class type as it
        // is guaranteed by the compiler
        if (this.id > temp.id)
        return 1;
        if (this.id < temp.id)
        return -1;
        else
        return 0;
    }
}

54 - Collection Initialisation

This uses a similar syntax to object initialisation:

List intList = new List { 3, 5, 1, 8, 5 };

A more involved example, which combines collection initialisation and object initialisation:

List myListOfPoints = new List
{
new Point { X = 2, Y = 2 },
new Point { X = 3, Y = 3 },
new Point(PointColor.BloodRed){ X = 4, Y = 4 }
};

Some generic collection classes:

  • Dictionary
  • LinkedList
  • List
  • Queue
  • SortedDictionary
  • SortedSet
  • Stack

55 - default

As well as being used in switch statements, the default keyword can be used to set a value to the correct default for the type:

t = default(T);

This can be useful in generics if values need to be reset to defaults.

56 - Generic Type Constraints

When declaring a generic type, a 'where' clause can be added to restrict the allowed interfaces that the type can support. For example, the following generic only supports type which are classes, support the IDrawable interface, and have a default (parameter-less) constructor:

public class MyGenericClass where T : class, IDrawable, new()
{
    ...
}

Constraints:

  • where T : struct

The type parameter must have System.ValueType in its chain of inheritance (i.e., must be a structure).

  • where T : class

The type parameter must not have System.ValueType in its chain of inheritance (i.e., must be a reference type).

where T : new()

The type parameter must have a default constructor. This is helpful if your generic type must create an instance of the type parameter because you cannot assume you know the format of custom constructors. Note that this constraint must be listed last on a multiconstrained type.

where T : NameOfBaseClass

The type parameter must be derived from the class specified by NameOfBaseClass.

where T : NameOfInterface

The type parameter must implement the interface specified by NameOfInterface.

You can separate multiple interfaces as a comma-delimited list.

If a generic has multiple parameters (such as the Dictionary collection), there can be 'where' clauses for each parameter:

public class MyGenericClass where K : SomeBaseClass, new()
where T : struct, IComparable
{
    ...
}

57 - Custom Generic Methods

As well as using the existing collections and methods you can create your own generic methods:

static void Swap(ref T a, ref T b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

Note that it is possible to omit the generic reference when calling a generic method which takes parameters, as the compiler can infer the correct method based on the types that you pass:

bool b1 = True;
bool b2 = False;
// This is allowed:
Swap(ref b1, ref b2);
// ...instead of this:
Swap(ref b1, ref b2);

However, this is not good practice as it tends to hide the type from anyone looking at the method call.

58 - Delegates

A delegate object can hold one or more references to methods of a specific signature. Invoking the delegate (which is done exactly as if the delegate object were a method itself) results in the contained methods being called.

When a delegate object is created, it is created using the signature that the methods will have. For example:

    public delegate int BinaryOperation(int x, int y);

This defines a delegate for methods which take two integer parameters and return an integer.

When a delegate object is created, an initial method can be passed to it:

   public int Add(int x, int y)
   {
       return x + y;
   }

   BinaryOperation opHandler = new BinaryOperation(this.Add);

More commonly, though, methods are assigned to the handler using the += operator:

   BinaryOperation opHandlerList;

   opHandlerList += this.Add;

Delegates are commonly used for event-handling. When an event occurs, the details of the event are passed to all the delegates that have registered as being interested. A common pattern is to provide a Register method which accepts a delegate object of the required type, and stores it in the list:

    RegisterBinaryOperation(BinaryOperation m)
    {
        opHandlerList += m;
    }

    RegisterBinaryOperation(new BinaryOperation(this.Add));

It is also possible to use 'method group conversion' which simply allows the method itself to be passed, instead of the delegate object:

   RegisterBinaryOperation(this.Add);

Delegates can also use generics:

   public delegate void Observer(T arg);

Delegates are essentially an implementation of the Observer pattern.

Note that unlike Delphi's event-handlers (which delegates are the equivalent of), delegates can handle multiple observers.

59 - Action and Func Delegates

There are predefined Action and Func generic delegates which can be used instead of creating your own. Action takes up to 16 parameters, but returns void, whereas Func takes up to 16 parameters and returns a value.

SimpleMath m = new SimpleMath();
Func OnAdd = new Func(m.Add);

Note that for Func the last parameter is the return type.

60 - Anonymous Methods

When defining the methods that will be assigned to method lists, you can use anonymous methods instead:

observable.OnEvent += delegate(object sender, Args e)
    {
        Console.WriteLine("From anonymous method: {0}", e.Message);
    };

Note that anonymous methods cannot access ref or out parameters of the method in which they are defined, but they can access other parameters and local variables. In other respects their scope is as with normal methods.

61 - Lambdas

Lambdas are essentially simplified anonymous methods. The following example uses a lambda instead of an anonymous method, to provide the callback for the List.FindAll() method (this illustrates another use for delegates, as standard callback mechanisms):

// Make a list of integers.
List list = new List();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// Now process each argument within a group of
// code statements.
List evenNumbers = list.FindAll((i) =>
{
    Console.WriteLine("value of i is currently: {0}", i);
    bool isEven = ((i % 2) == 0);
    return isEven;
});

Here is an even simpler version:

// Make a list of integers.
List list = new List();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// Now process each argument within a group of
// code statements.
List evenNumbers = list.FindAll((i) => ((i % 2) == 0));

Note that the following is also acceptable, but possibly not good practice:

List evenNumbers = list.FindAll(i => (i % 2) == 0);

Note that type inference is used to determine the type of the 'i' parameter, but this could be explicitly indicated instead:

List evenNumbers = list.FindAll((int i) => (i % 2) == 0);

Any method that takes a delegate object can take a lambda expression instead.

62 - Events

Instead of providing Register and Unregister methods (which have to be manually coded for each class they appear in), you can define the delegates as events, which provides these methods automatically.

public class SimpleArgs: EventArgs
{
    public readonly string Message;
    public CountDownArgs(string message)
        {
            Message = message;
        }
}

public class Observable
{
    public event Action OnEvent;

    public void DoSomething()
    {
        if (OnEvent != null)
            OnEvent(this, new SimpleArgs("Something was done!"));
    }
}

class Program
{
    static void Main(string[] args)
        {
            Observable o = new Observable();
            o.OnEvent += OnEvent;

            // Using an anonymous method
            o.OnEvent += delegate(object sender, SimpleArgs e)
            {
                if (sender is Observable)
            }; // Note the final semi-colon!

            o.DoSomething();
        }

    static void OnEvent(object sender, SimpleArgs e)
        {
            if (sender is Observable)
                Console.WriteLine(e.Message);
        }

}

63 - Indexer Methods

These allow a class to be treated as if it were a collection or array, by adding properties which can return elements from an internal collection on the basis of an index.

public class PersonCollection : IEnumerable
{
    // Internal array
    private ArrayList arPeople = new ArrayList();

    // Custom indexer for this class.
    public Person this[int index]
    {
        get { return (Person)arPeople[index]; }
        set { arPeople.Insert(index, value); }
    }
}

This allows the class to be accessed via an index:

people = new PersonCollection();

// ... add entries ...

// Access the entries
Console.WriteLine(people[0].Name);

Indexers can also be strings:

public class PersonCollection : IEnumerable
{
    private Dictionary listPeople =
        new Dictionary();

    // This indexer returns a person based on a string index.
    public Person this[string name]
    {
        get { return (Person)listPeople[name]; }
        set { listPeople[name] = value; }
    }
}

The above example could actual be done using the Dictionary itself, but it illustrates the point.

It is possible to overload indexers, so you could have a class which allowed access via an integer index or a string -- you simply define multiple accessor methods for the index property.

Multi-dimensional indexes are also possible:

public class SomeContainer
{
    private int[,] my2DintArray = new int[10, 10];

    public int this[int row, int column]
    {
        /* get or set value from 2D array */
    }
}

Indexers can be included in interfaces. The implementing classes must provide the appropriate accessor methods as normal:

public interface IStringContainer
{
    string this[int index] { get; set; }
}

64 - Overloading Operators

Where classes have obvious but non-supported operations that match with the existing operators it is possible to overload the operators and provide the required functionality:

// Overloaded operator +.
public static Point operator + (Point p1, Point p2)
{
    return new Point(p1.X + p2.X, p1.Y + p2.Y);
}

Note that += and -= are automatically provided if the relevant + and/or - operators are overloaded. They cannot be overloaded otherwise.

65 - Overloading == and !=

public class Point
{
    ...
    public override bool Equals(object o)
    {
        return o.ToString() == this.ToString();
    }

    public override int GetHashCode()
    {
        return this.ToString().GetHashCode();
    }

    public static bool operator ==(Point p1, Point p2)
    {
        return p1.Equals(p2);
    }

    public static bool operator !=(Point p1, Point p2)
    {
        return !p1.Equals(p2);
    }
}

66 - Custom Conversion Routines

You can create custom conversion routines to allow explicit and implicit type-casting between types which could not otherwise be converted.

The following is intended to be a method in a Square struct which will accept a Rectangle struct and convert it, allowing a typecast from Rectangle to Square (which would otherwise not be possible as structs cannot inherit).

public static explicit operator Square(Rectangle r)
{
    Square s = new Square();
    s.Length = r.Height;
    return s;
}

If instead you provide a method for implicit conversion, no type-casting is required to convert from one type to the other.

The following is a method in a Rectangle struct which allows a Square to be implicitly cast to a Rectangle:

public static implicit operator Rectangle(Square s)
{
    Rectangle r = new Rectangle();
    r.Height = s.Length;

    // Assume the length of the new Rectangle with
    // (Length x 2).
    r.Width = s.Length * 2;
    return r;
}

67 - Extension Methods

Extension methods allow new methods to be added to existing classes without inheritance. They can also be applied to value types, as in this example:

static class ExtensionMethods
{
    // This method allows any integer to reverse its digits.
    // For example, 56 would return 65.
    public static int ReverseDigits(this int i)
    {
        // Translate int into a string, and then
        // get all the characters.
        char[] digits = i.ToString().ToCharArray();
        // Now reverse items in the array.
        Array.Reverse(digits);
        // Put back into string.
        string newDigits = new string(digits);
        // Finally, return the modified string back as an int.
        return int.Parse(newDigits);
    }
}

The first parameter of an extension method uses 'this' and specifies the type that the method can be applied to. Other parameters can follow, but these will be standard parameters.

The extension method can then be applied to an instance of the relevant type:

int anInt = 123456;
Console.WriteLine("Reversed: {0}", anInt.ReverseDigits());

Usually extension methods will be in a namespace and an assembly so that they can be referenced by units which require them, and do not impact anything else.

Extensions can also be applied to interfaces:

public static void PrintData(this System.Collections.IEnumerable iterator)
{
    foreach (var item in iterator)
    {
        Console.WriteLine(item);
    }
}

Any IEnumerable or IEnumerable<> can then reference this method.

68 - Anonymous Types

Anonymous types allow minimal structures to be created 'on the fly', like so:

static void BuildAnonType( string make, string color )
{
    // Build anon type using incoming args plus on hard-coded value
    var car = new { Make = make, Color = color, Speed = 50 };

    // Note you can now use this type to get the property data!
    Console.WriteLine("You have a {0} {1} going {2} MPH",
    car.Color, car.Make, car.Speed);

    // Anon types have custom implementations of each virtual
    // method of System.Object. For example:
    Console.WriteLine("ToString() == {0}", car.ToString());
}

All anonymous types descend from System.Object and hence inherit all its properties (such as the ToString() method as shown above).

One common use of anonymous types is to create instances that are immediately assigned as items of a collection.

Equality of Anonymous Objects

Given two anonymous objects in the same assembly, with the same properties and property values:

  • Equals() will say they are equal
  • == will say they are not equal (they are different instances)
  • GetType() will return the same name for both (i.e. they are the same type).

Given two anonymous objects with the same properties but different values:

  • Equals() will say they are not equal
  • == will say they are not equal
  • GetType() will say they are the same type

Given two anonymous objects with different properties, all three checks will say that they are different and of different types.

Anonymous types can contain other anonymous types.

Directories and Files

System.IO.Directory.Exists(path)