Bluffer's Guide to C# 2
Ref: http://csharpindepth.com/Articles/General/BluffersGuide2.aspx
Obviously C# in Depth teaches the new features of C# 2 in depth. However, if you want to bluff it for the moment (just until your copy of the book arrives, of course) - welcome to the bluffer's guide. This isn't intended to give you enough information to be useful when coding - but you can pretend you know what you're talking about in suitably geeky company. More seriously, it will give you a very rough overview to give some context if you choose to investigate a particular feature further. (See also: Bluffer's Guide to C# 3.)
Each of the major features of C# 2 is here, with:
- A brief description of the feature, usually with an example
- A couple of "extra bluff power" phrases to throw into conversation to get bonus points from your listeners - so long as they don't quiz you further
- A couple of "call their bluff" claims which sound plausible, but are actually inaccurate. If you feel someone else is perhaps overstating their experience, see if they spot these traps.
Generics
Description
Generics allow types and methods to be parameterised by other types. Frankly it's a difficult idea to describe in few words, but it's best shown with an example:
// Without generics: ArrayList
ArrayList listOfStrings = new ArrayList();
listOfStrings.Add("first");
listOfStrings.Add("second");
listOfStrings.Add("third");
listOfStrings.Add(4); // Eek!
// Cast is necessary, as indexer just returns object
string y = (string) listOfStrings[1];
// This will blow up when it reaches 4
foreach (string x in listOfStrings)
{
Console.WriteLine(x);
}
// With generics: List<T>
List<string> listOfStrings = new List<string>();
listOfStrings.Add("first");
listOfStrings.Add("second");
listOfStrings.Add("third");
// Compilation error! Compiler knows the list should only contain strings
listOfStrings.Add(4);
// No cast necessary: the list can only contain strings
string y = listOfStrings[1];
// This is guaranteed to be okay
foreach(string x in listOfStrings)
{
Console.WriteLine(x);
}
Extra bluff power
- When writing a generic type or method, you can add constraints to type parameters. For instance,
Nullable<T>
makes sure thatT
is a value type. - Generics in don't have variance in C# 2 or 3 - you can't convert from
IEnumerable<string>
toIEnumerable<object>
for example, even though every string is an object. This is a complex issue which is likely to be partially addressed in C# 4. The capability already exists within the CLR.
Call their bluff (untrue statements)
- Generics are just syntactic sugar - casts and checks performed by the compiler. The runtime doesn't know anything about them. (Reality: that's broadly true in Java, but not in .NET. The .NET generics design keeps a lot more information than the Java version, giving it a lot more power in many areas.)
- Generics can be applied to all types. (Reality: you can declare generic classes, interfaces, delegates and structs - but not generic enums.)
Nullable types
Nullable types are value types which are wrappers round other value types (where the other type is specified with generics), allowing a "null value" to be represented. They are particularly useful with database code, as databases often have nullable columns for numbers, dates and times, GUIDs etc - all of which are value types in .NET. The Nullable<T>
structure is at the heart of nullable types, and C# has extra syntactic sugar with the "?" modifier and various other neat features. For example:
// x, y and z are the same type
int? x = null;
Nullable<int> y = 10;
int? z = x+y;
if (z==null)
{
Console.WriteLine("Null result");
}
else
{
// Note: not nullable
int result = z.Value;
Console.WriteLine("Result: {0}", result);
}
Extra bluff power
- The CLR knows about nullable types too - it makes sure that if you box the null value of a nullable type, you end up with a null reference. (And likewise you can unbox a null reference to the null value of a nullable type.)
- How operators work with nullable types is a language decision. For instance, comparing two
int?
values for equality always gives true or false in C#; in VB.NET it can result in null (if either side is null). Be careful when porting code!
Call their bluff (untrue statements)
Nullable<T>
constrains<T>
to be a value type, but it's a value type itself. That means you can writeNullable<Nullable<Nullable<int>>>
even though it's not very useful. (Reality: a value type constraint explicitly excludes any nullable types.)- The null coalescing operator (
??
) can only be used with a nullable type on the left hand side and its underlying non-nullable type on the right hand side. (Reality: the null coalescing operator can be used with reference types, and the right hand side can also be a reference type or a nullable type. It can be useful to use the operator several times in a row, e.g.first ?? second ?? third
. Indeed, the language is designed to enable this to work exactly how you'd like it to.)
Delegates (method group conversions and anonymous methods)
Description
Delegates in C# 2 have a number of improvements over C# 1. The most important two features are implicit method group conversions and anonymous methods, both of which make it easier to create new instances of delegate types. For example:
public void Foo()
{
// Do some stuff
}
// C# 1 code:
ThreadStart ts1 = new ThreadStart(Foo);
// C# 2 code, implicit method group conversion:
ThreadStart ts2 = Foo;
// C# 2, anonymous method:
ThreadStart ts3 = delegate { Console.WriteLine("Hi!"); };
There's a lot more to anonymous methods than meets the eye, by the way...
Extra bluff power
- Anonymous methods act as closures, allowing the delegate code to interact with the context in which it was created - including local variables.
- Delegate construction in C# 2 allows covariance/contravariance - so you can build a
KeyPressEventHandler
from anEventHandler
, for instance - any parameters that aKeyPressEventHandler
can handle will be valid for anEventHandler
too.
Call their bluff (untrue statements)
- Anonymous methods just add methods to the classes in which they're declared. (Reality: sometimes this is the case, but if local variables are captured then extra types may be created.)
- Anonymous methods can't update captured variables, just like Java's anonymous classes. (Reality: the C# variable capture is much more subtle than Java's form. In Java, the variable's value is captured; in C# the variable itself is captured.)
Iterator blocks
Description
Iterator blocks allow IEnumerable<T>
, IEnumerable
, IEnumerator<T>
and IEnumerator
to be implemented very simply in C# code. For example, the code below will print out "start", "0", "1", "2", "3", "4", "end".
IEnumerable<string> GetSequence()
{
yield return "start";
for (int i=0; i < 5; i++)
{
yield return i.ToString();
}
yield return "end";
}
...
foreach (string x in GetSequence())
{
Console.WriteLine(i);
}
Extra bluff power
- The compiler creates a new type for the iterator, which has member variables for the local variables in the iterator block.
yield return
effectively "pauses" the method - next timeMoveNext
is called (which happens implicitly inforeach
statements) execution resumes from just after theyield return
.
Call their bluff (untrue statements)
- When the method is called (
GetSequence()
in the above example), all the code actually runs, and the results just get buffered into aList<T>.
(Reality: such a scheme would completely defeat the point of iterators, and be impossible for infinite sequences. In fact, when the method is called almost nothing happens - an instance of the compiler-generated type is created and returned, but no user code executes. That starts happening whenMoveNext()
is called the first time.) - The CLR has special stack-handling code to keep the stack frame when the method was first called, so that the state can be kept. (Reality: the CLR doesn't care that this is a compiler-generated iterator. The smarts are in the C# compiler, which converts the state required by the iterator block into member variables in the extra iterator type.)
Partial types
Partial types allow a single type to be built from multiple source files. This is particularly useful with autogenerated code, where the tool (e.g. a GUI designer) can "own" one file, and the developer can work in a different one. The partial
keyword is used to indicate that the type may span multiple files.
// Partial1.cs
public partial class Partial
{
public void Foo()
{
Bar(); // Calls into a method declared in a different type
}
}
// Partial2.cs
public partial class Partial
{
void Bar()
{
Console.WriteLine("Hi!");
}
}
Extra bluff power
- Partial types apply to interfaces, classes and structs - but not enums or delegates.
- Different files can indicate that a class implements different interfaces. The implementation of an interface doesn't have to be in the same file.
Call their bluff (untrue statements)
- Partial types spanning not just different source files, but different assemblies provide a very powerful feature. (Reality: partial types are just a compile-time feature. A type itself still lives wholly within a single assembly.)
- Partial types cannot be generic. (Reality: they can be generic, and the type parameters can have constraints applied - but you can't specify some constraints in one file and different constraints in another.)