Q: What do delegates buy you? A: Delegates enable scenarios that some other languages have addressed with function pointers. However, unlike function pointers, delegates are object-oriented and type-safe.
|
Q: Are delegates really type-safe? Not really. A delegate instance does not know or care about the classes of the methods it encapsulates; all that matters is that those methods be compatible with the delegate's type.
|
Q: What does that mean? The problem is best illustrated with an example:
| using System;
class Department { public delegate void Fireable();
public static void FireEmployee(Fireable fireable) { fireable(); } }
class Military { public delegate void Fireable();
public static void FireMissile(Fireable fireable) { fireable(); } }
class Missile { public void Fire() { Console.WriteLine("missile fired"); } }
class Employee { public void Fire() { Console.WriteLine("employee fired"); } }
class Test { static Employee e1 = new Employee(); static Missile e2 = new Missile();
static void Main(string[] args) { Department.Fireable e = new Department.Fireable(e2.Fire); Department.FireEmployee(e); } }
| In this program, the programmer has made an error. The line
| Department.Fireable e = new Department.Fireable(e2.Fire);
| really should be
| Department.Fireable e = new Department.Fireable(e1.Fire);
| However the compiler does not detect this coding error. The program compiles fine and you get this output when you run it: Uh oh! The programmer intended to fire an employee, but he has launched a missile instead! |
Q: Is there a better way to do this? A: Yes. You can get type safety by using interfaces instead of delegates.
| using System;
class Department { public interface Fireable { void Fire(); } public static void FireEmployee(Fireable fireable) { fireable.Fire(); } }
class Military { public interface Fireable { void Fire(); } public static void FireMissile(Fireable fireable) { fireable.Fire(); } }
class Missile : Military.Fireable { public void Fire() { Console.WriteLine("missile fired"); } }
class Employee : Department.Fireable { public void Fire() { Console.WriteLine("employee fired"); } }
class Test { static Employee e1 = new Employee(); static Missile e2 = new Missile();
static void Main(string[] args) { Department.Fireable e = e1; Department.FireEmployee(e); } }
| Try changing the line
| Department.Fireable e = e1;
| to this:
| Department.Fireable e = e2;
| When you try to compile the program the compiler tells you:
| test.cs(38,33): error CS0029: Cannot implicitly convert type 'Missile' to 'Department.Fireable'
| The compiler has detected your coding error! |
Q: What is type safety all about? A: Type safety is about increasing the opportunities for the compiler to detect your coding errors. If you use interfaces instead of delegates the compiler will have more opportunities to detect your coding errors.
|
Q: What other differences exist between delegates and interfaces? A: Interfaces carry semantics, and when a programmer implements an interface, he is typically well aware of that semantics. When you try to invoke a particular method via an interface, you can be fairly certain that if you succeed, the semantics of that method is what you expect. For that reason, using interfaces is essentially doing a check for semantic correctness on some level. Delegates, on the other hand, by only verifying the method signature, make the programmer responsible for ensuring that the semantics of the method is compatible. The semantics may cover not only the meaning of the arguments and return value (some times even the order of the arguments if they are of the same type), the ranges of the arguments, but also an invocation order when multiple methods are concerned. Hence, in a sufficiently large program there is plenty of margin to make an error when different programmers are not forced to comply with a uniform semantics (as they would be if interfaces were used).
|
Q: What can we conclude from all this? A: Use of delegates results in shorter but less reliable code.
|
C# FAQ Q: What does boxing and unboxing buy you?A: Boxing and unboxing enable
type system unification.
Q: What is type system unification?The goal of type system unification is to bridge the gap between value types and reference types that exists in most languages. For example, a Stack class can provide Push and Pop methods that take and return object values.
| public class Stack { public object Pop() {...} public void Push(object o) {...} }
|
Because C# has a unified type system, the Stack class can be used with elements of any type, including value types like int.
Q: Does boxing and unboxing really bridge the gap between value types and reference types?No, not really. Reference types have the concept of identity and equality, while value types only have equality. Boxing and unboxing does not change this fundamental difference.
Autoboxing in C# allows value types to go back and forth between being objects, but each time a value type becomes an object it acquires a new identity. Here's a sample program to illustrate this problem:
| using System;
struct A { }
class B { }
public class Test { static Object Process(Object o) { if (o is A) { A a = (A)o; // do something with a return a; } else if (o is B) { B b = (B)o; // do something with b return b; } return null; }
static void Check(Object o) { Console.WriteLine(o == Process(o)); }
public static void Main(string[] args) { Check(new A()); Check(new B()); } }
|
This program prints
which may be unexpected if you don't understand that value types and reference types cannot be treated the same way. This illustrates the fact that C# does not bridge the gap between value types and reference types.
The solution to this problem is to use wrapper classes. Pretending there is no difference between reference types and value types is dangerous as it will invariably result in hard-to-detect bugs, as demonstrated by this example.
Q: What other pitfalls should I be aware of?A: A boxing conversion always makes a copy of the value being boxed. This is different from a conversion of a reference-type to type object, in which the value continues to reference the same instance. For example, given the declaration
| struct Point { public int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }
|
the following statements
| Point p = new Point(10, 10); object box = p; p.x = 20; Console.Write(((Point)box).x);
|
will output the value 10 because the implicit boxing operation that occurs in the assignment of p to box causes the value of p to be copied. Had Point been declared a class instead, the value 20 would be output because p and box would reference the same instance.
Q: Isn't it cool to have collections which can take both ints as well as strings?A: Yes, but note that if you store a value type in a collection, then in order to update the value you have to unbox, update the value, rebox the updated value and then replace the object in the collection. This has two problems:
- Performance. As this article shows, you can almost double performance by using wrapper classes (see IntHolderClass) instead of autoboxing.
- Loss of identity. The updated object is not the same as the old object. This can break code that depends on the identity of the object. Again, this problem can also be solved by using a wrapper class.
Q: What can we conclude from all this? A: The 'Type System Unification' in C# is half-baked and full of pitfalls.
C# FAQ Q: What is the problem with checked exceptions?
A: With checked exceptions, you have to either catch exceptions thrown by the methods you call, or mention the exception in the throws clause of your method. Because of the tedium of mentioning each exception in the throws clause, some programmers may take the shortcut of
swallowing exceptions.
Q: What is meant by swallowing an exception?
A: You should only catch exceptions that you are prepared to handle. Otherwise, if you catch an exception and ignore it (or log it), you are "swallowing" the exception. As a result, code higher up the call chain that may be prepared to handle the exception will never see the exception.
Q: Does C# solve the problem of swallowed exceptions?
A: No. In fact, C# requires it! Methods in C# cannot specify the exceptions it may throw. The documentation of the method you wish to call may mention the list of exceptions you can expect. But this list of exceptions is not enforced by the compiler, and is not guaranteed to be exhaustive. Also, since the list of exceptions is not part of the contract, the method can be revised at any time and a new exception that you didn't know about can be thrown.
Since there is no previously agreed upon list of exceptions, you are left with no option but to catch all exceptions if you want to recover from an exception (such as by offering to connect to an alternate database if the primary database is down, etc.)
Obviously, if you catch all exceptions, you will end up swallowing some of them.
Q: What other problems exist with C# exceptions?
A: Often in C#, if you have multiple implementations of an interface, each of those implementations may have their own disjoint exception hierarchies. (i.e., the exception hierarchies have no common classes other than System.Exception.) Example: ADO.NET. This makes it impossible for generic code (i.e., code that can use any of those implementations) to recover from exceptions generically without catching
all exceptions (i.e., System.Exception.)
Again, if you catch all exceptions you will end up swallowing some of them.
Q: What can we conclude from all this?
A: Unchecked exceptions results in shorter but less robust code.