Chapter 4 - Advanced C#
Delegates
Delegates
// A delegate type declaration is like an abstract method declaration, prefixed with the delegate keyword.
// To create a delegate instance, assign a method to a delegate variable:
Transformer t = Square; // Create delegate instance
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
int Square (int x) => x * x;
delegate int Transformer (int x); // Our delegate type declaration
Delegates - longhand
Transformer t = new Transformer (Square); // Create delegate instance
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
int Square (int x) => x * x;
delegate int Transformer (int x); // Delegate type declaration
Delegates - Writing Plug-in Methods
// A delegate variable is assigned a method dynamically. This is useful for writing plug-in methods:
int[] values = { 1, 2, 3 };
Transform (values, Square); // Hook in the Square method
values.Dump();
values = new int[] { 1, 2, 3 };
Transform (values, Cube); // Hook in the Cube method
values.Dump();
void Transform (int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values [i] = t (values [i]);
}
int Square (int x) => x * x;
int Cube (int x) => x * x * x;
delegate int Transformer (int x);
Static method target
Transformer t = Test.Square;
Console.WriteLine (t(10)); // 100
class Test
{
public static int Square (int x) => x * x;
}
delegate int Transformer (int x);
Instance method target
// When a delegate object is assigned to an instance method, the delegate object must maintain
// a reference not only to the method, but also to the instance to which the method belongs:
MyReporter r = new MyReporter();
r.Prefix = "%Complete: ";
ProgressReporter p = r.ReportProgress;
p(99); // 99
Console.WriteLine (p.Target == r); // True
Console.WriteLine (p.Method); // Void InstanceProgress(Int32)
r.Prefix = "";
p(99); // 99
public delegate void ProgressReporter (int percentComplete);
class MyReporter
{
public string Prefix = "";
public void ReportProgress (int percentComplete) => Console.WriteLine (Prefix + percentComplete);
}
Multicast Delegates
// All delegate instances have multicast capability:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
d();
" -- SomeMethod1 and SomeMethod2 both fired\r\n".Dump();
d -= SomeMethod1;
d();
" -- Only SomeMethod2 fired".Dump();
void SomeMethod1 () => "SomeMethod1".Dump();
void SomeMethod2 () => "SomeMethod2".Dump();
delegate void SomeDelegate();
Multicast Delegates - ProgressReporter
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile;
Util.HardWork (p);
void WriteProgressToConsole (int percentComplete)
{
Console.WriteLine (percentComplete);
}
void WriteProgressToFile (int percentComplete)
{
System.IO.File.WriteAllText ("progress.txt", percentComplete.ToString());
}
delegate void ProgressReporter (int percentComplete);
class Util
{
public static void HardWork (ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p (i * 10); // Invoke delegate
System.Threading.Thread.Sleep (100); // Simulate hard work
}
}
}
Generic Delegate Types
// A delegate type may contain generic type parameters:
int[] values = { 1, 2, 3 };
Util.Transform (values, Square); // Dynamically hook in Square
values.Dump();
int Square (int x) => x * x;
public delegate T Transformer<T> (T arg);
// With this definition, we can write a generalized Transform utility method that works on any type:
public class Util
{
public static void Transform<T> (T[] values, Transformer<T> t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
}
Func and Action Delegates
// With the Func and Action family of delegates in the System namespace, you can avoid the
// need for creating most custom delegate types:
int[] values = { 1, 2, 3 };
Util.Transform (values, Square); // Dynamically hook in Square
values.Dump();
int Square (int x) => x * x;
public class Util
{
// Define this to accept Func<T,TResult> instead of a custom delegate:
public static void Transform<T> (T[] values, Func<T, T> transformer)
{
for (int i = 0; i < values.Length; i++)
values [i] = transformer (values [i]);
}
}
Delegates vs Interfaces
// A problem that can be solved with a delegate can also be solved with an interface:
int[] values = { 1, 2, 3 };
Util.TransformAll (values, new Squarer());
values.Dump();
public interface ITransformer
{
int Transform (int x);
}
public class Util
{
public static void TransformAll (int[] values, ITransformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t.Transform (values[i]);
}
}
class Squarer : ITransformer
{
public int Transform (int x) => x * x;
}
Delegates vs Interfaces - Clumsiness
// With interfaces, we’re forced into writing a separate type per transform
// since Test can only implement ITransformer once:
int[] values = { 1, 2, 3 };
Util.TransformAll (values, new Cuber());
values.Dump();
public interface ITransformer
{
int Transform (int x);
}
public class Util
{
public static void TransformAll (int[] values, ITransformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t.Transform (values[i]);
}
}
class Squarer : ITransformer
{
public int Transform (int x) => x * x;
}
class Cuber : ITransformer
{
public int Transform (int x) => x * x * x;
}
Delegate Type Incompatibility
// Delegate types are all incompatible with each other, even if their signatures are the same:
D1 d1 = Method1;
D2 d2 = d1; // Compile-time error
static void Method1() { }
delegate void D1();
delegate void D2();
Delegate Type Incompatibility - Workaround
// Delegate types are all incompatible with each other, even if their signatures are the same:
D1 d1 = Method1;
D2 d2 = new D2 (d1); // Legal
void Method1() { }
delegate void D1();
delegate void D2();
Delegate Equality
// Delegate instances are considered equal if they have the same method targets:
D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2); // True
static void Method1() { }
delegate void D();
Parameter Compatibility (Contravariance)
// A delegate can have more specific parameter types than its method target. This is called contravariance:
delegate void StringAction (string s);
static void Main()
{
StringAction sa = new StringAction (ActOnObject);
sa ("hello");
}
static void ActOnObject (object o) => Console.WriteLine (o); // hello
Return Type Compatibility (Covariance)
// A delegate can have more specific parameter types than its method target. This is called contravariance:
ObjectRetriever o = new ObjectRetriever (RetriveString);
object result = o();
Console.WriteLine (result); // hello
string RetriveString() => "hello";
delegate object ObjectRetriever();
Type Parameter Variance
/* From C# 4.0, type parameters on generic delegates can be marked as covariant (out) or contravariant (in).
For instance, the System.Func delegate in the Framework is defined as follows:
public delegate TResult Func<out TResult>();
This makes the following legal: */
Func<string> x = () => "Hello, world";
Func<object> y = x;
/* The System.Action delegate is defined as follows:
void Action<in T> (T arg);
This makes the following legal: */
Action<object> x2 = o => Console.WriteLine (o);
Action<string> y2 = x2;
Events
Events
// The easiest way to declare an event is to put the event keyword in front of a delegate member.
// Code within the containing type has full access and can treat the event as a delegate.
// Code outside of the containing type can only perform += and -= operations on the event.
var stock = new Stock ("MSFT");
stock.PriceChanged += ReportPriceChange;
stock.Price = 123;
stock.Price = 456;
void ReportPriceChange (decimal oldPrice, decimal newPrice)
{
("Price changed from " + oldPrice + " to " + newPrice).Dump();
}
public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);
public class Stock
{
string symbol;
decimal price;
public Stock (string symbol) { this.symbol = symbol; }
public event PriceChangedHandler PriceChanged;
public decimal Price
{
get { return price; }
set
{
if (price == value) return; // Exit if nothing has changed
decimal oldPrice = price;
price = value;
if (PriceChanged != null) // If invocation list not empty,
PriceChanged (oldPrice, price); // fire event.
}
}
}
Standard Event Pattern
// There's a standard pattern for writing events. The pattern provides
// consistency across both Framework and user code.
Stock stock = new Stock ("THPW");
stock.Price = 27.10M;
// Register with the PriceChanged event
stock.PriceChanged += stock_PriceChanged;
stock.Price = 31.59M;
void stock_PriceChanged (object sender, PriceChangedEventArgs e)
{
if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M)
Console.WriteLine ("Alert, 10% stock price increase!");
}
public class PriceChangedEventArgs : EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangedEventArgs (decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice; NewPrice = newPrice;
}
}
public class Stock
{
string symbol;
decimal price;
public Stock (string symbol) {this.symbol = symbol;}
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged (PriceChangedEventArgs e)
{
PriceChanged?.Invoke (this, e);
}
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
decimal oldPrice = price;
price = value;
OnPriceChanged (new PriceChangedEventArgs (oldPrice, price));
}
}
}
Standard Event Pattern - Simple EventHandler
// The predefined nongeneric EventHandler delegate can be used when an event doesn't
// carry extra information:
Stock stock = new Stock ("THPW");
stock.Price = 27.10M;
// Register with the PriceChanged event
stock.PriceChanged += stock_PriceChanged;
stock.Price = 31.59M;
void stock_PriceChanged (object sender, EventArgs e)
{
Console.WriteLine ("New price = " + ((Stock)sender).Price);
}
public class Stock
{
string symbol;
decimal price;
public Stock (string symbol) { this.symbol = symbol; }
public event EventHandler PriceChanged;
protected virtual void OnPriceChanged (EventArgs e)
{
PriceChanged?.Invoke (this, e);
}
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
price = value;
OnPriceChanged (EventArgs.Empty);
}
}
}
Event Accessors
// We can take over the default event implementation by writing our own accessors:
Stock stock = new Stock ("THPW");
stock.Price = 27.10M;
// Register with the PriceChanged event
stock.PriceChanged += stock_PriceChanged;
stock.Price = 31.59M;
void stock_PriceChanged (object sender, EventArgs e)
{
Console.WriteLine ("New price = " + ((Stock)sender).Price);
}
public class Stock
{
string symbol;
decimal price;
public Stock (string symbol) { this.symbol = symbol; }
private EventHandler _priceChanged; // Declare a private delegate
public event EventHandler PriceChanged
{
add { _priceChanged += value; } // Explicit accessor
remove { _priceChanged -= value; } // Explicit accessor
}
protected virtual void OnPriceChanged (EventArgs e)
{
_priceChanged?.Invoke (this, e);
}
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
price = value;
OnPriceChanged (EventArgs.Empty);
}
}
}
Event Accessors - Interfaces
// When explicitly implementing an interface that declares an event, you must use event accessors:
public interface IFoo { event EventHandler Ev; }
class Foo : IFoo
{
private EventHandler ev;
event EventHandler IFoo.Ev
{
add { ev += value; }
remove { ev -= value; }
}
}
Lambda Expressions
Lambda Expressions
// A lambda expression is an unnamed method written in place of a delegate instance.
// A lambda expression has the following form:
// (parameters) => expression-or-statement-block
Transformer sqr = x => x * x;
Console.WriteLine (sqr (3)); // 9
// Using a statement block instead:
Transformer sqrBlock = x => { return x * x; };
Console.WriteLine (sqr (3));
// Using a generic System.Func delegate:
Func<int, int> sqrFunc = x => x * x;
Console.WriteLine (sqrFunc (3));
// Zero arguments:
Func<string> greeter = () => "Hello, world";
Console.WriteLine (greeter());
// With implicit typing (from C# 10):
var greeter2 = () => "Hello, world";
Console.WriteLine (greeter());
// Using multiple arguments:
Func<string, string, int> totalLength = (s1, s2) => s1.Length + s2.Length;
int total = totalLength ("hello", "world");
total.Dump ("total");
// Explicitly specifying parameter types:
Func<int, int> sqrExplicit = (int x) => x * x;
Console.WriteLine (sqrFunc (3));
delegate int Transformer (int i);
Lambda Expressions - Default Parameters
// Just as ordinary methods can have optional parameters:
void Print (string message = "") => Console.WriteLine (message);
// So, too, can lambda expressions:
var print = (string message = "") => Console.WriteLine (message);
print ("Hello");
print ();
// This feature is useful with libraries such as ASP.NET Minimal API.
Capturing Outer Variables
// A lambda expression can reference the local variables and parameters of the method
// in which it’s defined (outer variables)
int factor = 2;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier (3)); // 6
// Captured variables are evaluated when the delegate is invoked, not when the variables were captured:
factor = 10;
Console.WriteLine (multiplier (3)); // 30
// Lambda expressions can themselves update captured variables:
int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
Console.WriteLine (seed); // 2
Capturing Outer Variables - Lifetime
// Captured variables have their lifetimes extended to that of the delegate:
static Func<int> Natural()
{
int seed = 0;
return () => seed++; // Returns a closure
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
}
Capturing Outer Variables - Uniqueness
// A local variable instantiated within a lambda expression is unique per invocation of the
// delegate instance:
static Func<int> Natural()
{
return() => { int seed = 0; return seed++; };
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 0
}
Static lambdas
Func<int, int> multiplier = static n => n * 2;
multiplier (123).Dump();
Foo();
void Foo()
{
Multiply (123).Dump();
static int Multiply (int x) => x * 2; // Local static method
}
Capturing Iteration Variables
// When you capture the iteration variable in a for-loop, C# treats that variable as though it was
// declared outside the loop. This means that the same variable is captured in each iteration:
{
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
actions [i] = () => Console.Write (i);
foreach (Action a in actions) a(); // 333 (instead of 123)
}
// Each closure captures the same variable, i. When the delegates are later invoked, each delegate
// sees its value at time of invocation - which is 3. We can illustrate this better by expanding
// the for loop as follows:
{
Action[] actions = new Action[3];
int i = 0;
actions[0] = () => Console.Write (i);
i = 1;
actions[1] = () => Console.Write (i);
i = 2;
actions[2] = () => Console.Write (i);
i = 3;
foreach (Action a in actions) a(); // 333
}
Capturing Iteration Variables - Workaround
// The solution, if we want to write 012, is to assign the iteration variable to a local
// variable that’s scoped inside the loop:
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
int loopScopedi = i;
actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a(); // 012
Anonymous Methods
// Anonymous methods are a C# 2.0 feature that has been subsumed largely by C# 3.0 lambda expressions:
delegate int Transformer (int i);
static void Main()
{
// This can be done more easily with a lambda expression:
Transformer sqr = delegate (int x) { return x * x; };
Console.WriteLine (sqr(3)); // 9
}
// A unique feature of anonymous methods is that you can omit the parameter declaration entirely - even
// if the delegate expects them. This can be useful in declaring events with a default empty handler:
public static event EventHandler Clicked = delegate { };
// because it avoids the need for a null check before firing the event.
// The following is also legal:
static void HookUp()
{
Clicked += delegate { Console.WriteLine ("clicked"); }; // No parameters
}
try Statements and Exceptions
DivideByZeroException unhandled
// Because Calc is called with x==0, the runtime throws a DivideByZeroException:
int y = Calc (0);
Console.WriteLine (y);
int Calc (int x) { return 10 / x; }
DivideByZeroException handled
// We can catch the DivideByZeroException as follows:
try
{
int y = Calc (0);
Console.WriteLine (y);
}
catch (DivideByZeroException ex)
{
Console.WriteLine ("x cannot be zero");
}
Console.WriteLine ("program completed");
int Calc (int x) { return 10 / x; }
The catch Clause
// You can handle multiple exception types with multiple catch clauses:
static void Main() { MainMain ("one"); }
static void MainMain (params string[] args)
{
try
{
byte b = byte.Parse (args[0]);
Console.WriteLine (b);
}
catch (IndexOutOfRangeException)
{
Console.WriteLine ("Please provide at least one argument");
}
catch (FormatException)
{
Console.WriteLine ("That's not a number!");
}
catch (OverflowException)
{
Console.WriteLine ("You've given me more than a byte!");
}
}
static int Calc (int x) { return 10 / x; }
Exception Filters
try
{
await new HttpClient().GetStringAsync ("http://www.albahari.com/x");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
"Page not found".Dump();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.InternalServerError)
{
"Internal server error!".Dump();
}
catch (HttpRequestException ex)
{
$"Some other failure: {ex.StatusCode}".Dump();
}
The finally Block
// finally blocks are typically used for cleanup code:
File.WriteAllText ("file.txt", "test");
ReadFile ();
void ReadFile()
{
StreamReader reader = null; // In System.IO namespace
try
{
reader = File.OpenText ("file.txt");
if (reader.EndOfStream) return;
Console.WriteLine (reader.ReadToEnd());
}
finally
{
if (reader != null) reader.Dispose();
}
}
The using Statement
// The using statement provides an elegant syntax for calling Dispose on
// an IDisposable object within a finally block:
File.WriteAllText ("file.txt", "test");
ReadFile ();
void ReadFile()
{
using (StreamReader reader = File.OpenText ("file.txt"))
{
if (reader.EndOfStream) return;
Console.WriteLine (reader.ReadToEnd());
}
}
using Declarations
if (File.Exists ("file.txt"))
{
using var reader = File.OpenText ("file.txt");
Console.WriteLine (reader.ReadLine());
}
// reader is now disposed
Throwing Exceptions
// Exceptions can be thrown either by the runtime or in user code:
try
{
Display (null);
}
catch (ArgumentNullException ex)
{
Console.WriteLine ("Caught the exception");
}
static void Display (string name)
{
if (name == null)
throw new ArgumentNullException (nameof (name));
Console.WriteLine (name);
}
Shortcut for ArgumentNullException
// Exceptions can be thrown either by the runtime or in user code:
try
{
Display (null);
}
catch (ArgumentNullException ex)
{
ex.Dump ("Caught the exception");
}
void Display (string name)
{
ArgumentNullException.ThrowIfNull (name);
Console.WriteLine (name);
}
throw Expressions
// Prior to C# 7, throw was always a statement. Now it can also appear as an expression in
// expression-bodied functions:
ProperCase ("test").Dump();
ProperCase (null).Dump(); // throws an ArgumentException
string Foo() => throw new NotImplementedException();
// A throw expression can also appear in a ternary conditional expression:
string ProperCase (string value) =>
value == null ? throw new ArgumentException ("value") :
value == "" ? "" :
char.ToUpper (value [0]) + value.Substring (1);
Rethrowing an Exception
// Rethrowing lets you back out of handling an exception should circumstances turn out to be
// outside what you expected:
string s = null;
try
{
s = await new HttpClient().GetStringAsync ("http://www.albahari.com/x");
}
catch (HttpRequestException ex)
{
if (ex.StatusCode == HttpStatusCode.Forbidden)
Console.WriteLine ("Forbidden");
else
throw; // Can’t handle other cases, so rethrow
}
s.Dump();
Rethrowing More Specific Exception
//The other common scenario is to rethrow a more specific exception type:
DateTime dt;
string dtString = "2010-4-31"; // Assume we're writing an XML parser and this is from an XML file
try
{
// Parse a date of birth from XML element data
dt = XmlConvert.ToDateTime (dtString, XmlDateTimeSerializationMode.Utc);
}
catch (FormatException ex)
{
throw new XmlException ("Invalid DateTime", ex);
}
The TryXXX Pattern
bool result;
TryToBoolean ("Bad", out result).Dump ("Successful");
result = ToBoolean ("Bad"); // throws Exception
bool ToBoolean (string text)
{
bool returnValue;
if (!TryToBoolean (text, out returnValue))
throw new FormatException ("Cannot parse to Boolean");
return returnValue;
}
bool TryToBoolean (string text, out bool result)
{
text = text.Trim().ToUpperInvariant();
if (text == "TRUE" || text == "YES" || text == "Y")
{
result = true;
return true;
}
if (text == "FALSE" || text == "NO" || text == "N")
{
result = false;
return true;
}
result = false;
return false;
}
The Atomicity Pattern
Accumulator a = new Accumulator();
try
{
a.Add (4, 5); // a.Total is now 9
a.Add (1, int.MaxValue); // Will cause OverflowException
}
catch (OverflowException)
{
Console.WriteLine (a.Total); // a.Total is still 9
}
public class Accumulator
{
public int Total { get; private set; }
public void Add (params int[] ints)
{
bool success = false;
int totalSnapshot = Total;
try
{
foreach (int i in ints)
{
checked { Total += i; }
}
success = true;
}
finally
{
if (!success)
Total = totalSnapshot;
}
}
}
Enumeration and Iterators (see also CH7)
Enumeration
// High-level way of iterating through the characters in the word “beer”:
foreach (char c in "beer")
Console.WriteLine (c);
// Low-level way of iterating through the same characters:
using (var enumerator = "beer".GetEnumerator())
while (enumerator.MoveNext())
{
var element = enumerator.Current;
Console.WriteLine (element);
}
Collection Initializers
// You can instantiate and populate an enumerable object in a single step with collection initializers:
{
List<int> list = new List<int> {1, 2, 3};
list.Dump();
}
// Equivalent to:
{
List<int> list = new List<int>();
list.Add (1);
list.Add (2);
list.Add (3);
list.Dump();
}
Collection expressions
// From C# 12, you can use square brackets when initializing a array.
// This is called a collection expression:
int[] array = [1, 2, 3];
array.Dump();
// Collection expressions also work with other collection types:
List<int> list = [1, 2, 3];
list.Dump();
Span<int> span = [1, 2, 3];
span.Dump();
HashSet<int> set = [1, 2, 3];
set.Dump();
ImmutableArray<int> immutable = [1, 2, 3];
immutable.Dump();
// Unfortunately, collection expressions don't have a natural type, so the following is illegal:
// var foo = [1, 2, 3];
Collection expressions - target-typing
// Collection expressions are target-typed, meaning that the type of [1,2,3] depends on the
// type to which it’s assigned. This means that you can omit the type in other scenarios
// where the compiler can infer it, such as when calling methods:
Foo ([1, 2, 3]);
void Foo (List<int> numbers) => numbers.Dump();
// In the following example, we export timezones to Excel using LINQPad's ToSpreadsheet extension method,
// specifying the particular columns to include.
TimeZoneInfo.GetSystemTimeZones().ToSpreadsheet (membersToInclude: ["Id", "DisplayName", "BaseUtcOffset"]).Open();
Extra - Collection expressions - spread operator
// A collection expression can include other collections if prefixed by the .. (spread) operator:
string[] primaryLightColors = ["Red", "Green", "Blue"];
List<string> primaryPigments = ["Magenta", "Cyan", "Yellow"];
HashSet<string> allColors = [..primaryLightColors, ..primaryPigments, "Black", "White"];
allColors.Dump();
Collection Initializers - dictionaries
var dict1 = new Dictionary<int, string>()
{
{ 5, "five" },
{ 10, "ten" }
};
dict1.Dump();
var dict2 = new Dictionary<int, string>()
{
[3] = "three",
[10] = "ten"
};
dict2.Dump();
Iterators
// Whereas a foreach statement is a consumer of an enumerator, an iterator is a producer of an enumerator:
foreach (int fib in Fibs(6))
Console.Write (fib + " ");
IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
{
yield return prevFib;
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
yield break
// The yield break statement indicates that the iterator block should exit early,
// without returning more elements:
foreach (string s in Foo (true))
Console.WriteLine (s);
static IEnumerable<string> Foo (bool breakEarly)
{
yield return "One";
yield return "Two";
if (breakEarly)
yield break;
yield return "Three";
}
Multiple yield Statements
// Multiple yield statements are permitted:
foreach (string s in Foo())
Console.WriteLine (s); // Prints "One","Two","Three"
IEnumerable<string> Foo()
{
yield return "One";
yield return "Two";
yield return "Three";
}
Iterators and try-catch blocks
// A yield return statement cannot appear in a try block that has a catch clause:
foreach (string s in Foo())
Console.WriteLine (s);
IEnumerable<string> Foo()
{
try { yield return "One"; } // Illegal
catch { /*...*/ }
}
Iterators and try-finally blocks
// You can, however, yield within a try block that has (only) a finally block:
foreach (string s in Foo()) s.Dump();
Console.WriteLine();
foreach (string s in Foo())
{
("First element is " + s).Dump();
break;
}
IEnumerable<string> Foo()
{
try
{
yield return "One";
yield return "Two";
yield return "Three";
}
finally { "Finally".Dump(); }
}
Composing Iterators
// Iterators are highly composable:
foreach (int fib in EvenNumbersOnly (Fibs (6)))
Console.WriteLine (fib);
IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
{
yield return prevFib;
int newFib = prevFib + curFib;
prevFib = curFib;
curFib = newFib;
}
}
IEnumerable<int> EvenNumbersOnly (IEnumerable<int> sequence)
{
foreach (int x in sequence)
if ((x % 2) == 0)
yield return x;
}
// See Chapter 7 for more information on Iterators
Nullable (Value) Types
Nullable Types
// To represent null in a value type, you must use a special construct called a nullable type:
{
int? i = null; // Nullable Type
Console.WriteLine (i == null); // True
}
// Equivalent to:
{
Nullable<int> i = new Nullable<int>();
Console.WriteLine (! i.HasValue); // True
}
Implicit and Explicit Nullable Conversions
// The conversion from T to T? is implicit, and from T? to T is explicit:
int? x = 5; // implicit
int y = (int)x; // explicit
Boxing and Unboxing Nullable Values
// When T? is boxed, the boxed value on the heap contains T, not T?.
// C# also permits the unboxing of nullable types with the as operator:
object o = "string";
int? x = o as int?;
Console.WriteLine (x.HasValue); // False
Operator Lifting
// Despite the Nullable<T> struct not defining operators such as <, >, or even ==, the
// following code compiles and executes correctly, thanks to operator lifting:
int? x = 5;
int? y = 10;
{
bool b = x < y; // true
b.Dump();
}
// The above line is equivalent to:
{
bool b = (x.HasValue && y.HasValue) ? (x.Value < y.Value) : false;
b.Dump();
}
Operator Lifting - More Examples
// Operator lifting means you can implicitly use T’s operators on T? - without extra code:
int? x = 5;
int? y = null;
// Equality operator examples
Console.WriteLine (x == y); // False
Console.WriteLine (x == null); // False
Console.WriteLine (x == 5); // True
Console.WriteLine (y == null); // True
Console.WriteLine (y == 5); // False
Console.WriteLine (y != 5); // True
// Relational operator examples
Console.WriteLine (x < 6); // True
Console.WriteLine (y < 6); // False
Console.WriteLine (y > 6); // False
// All other operator examples
Console.WriteLine (x + 5); // 10
Console.WriteLine (x + y); // null
Operator Lifting - Equality Operators
// Lifted equality operators handle nulls just like reference types do:
Console.WriteLine ( null == null); // True
Console.WriteLine ((bool?)null == (bool?)null); // True
Operator Lifting - Relational Operators
// The relational operators work on the principle that it is meaningless to compare null operands:
int? x = 5;
int? y = null;
{
bool b = x < y;
b.Dump();
}
// Translation:
{
bool b = (x.HasValue && y.HasValue) ? (x.Value < y.Value) : false;
b.Dump();
}
All Other Operators (except for And+Or)
// These operators return null when any of the operands are null. This pattern should be familiar to SQL users:
int? x = 5;
int? y = null;
{
int? c = x + y;
c.Dump();
}
// Translation:
{
int? c = (x.HasValue && y.HasValue)
? (int?) (x.Value + y.Value)
: null;
c.Dump();
}
Mixing Nullable and Nonnullable Operators
// You can mix and match nullable and non-nullable types
// (this works because there is an implicit conversion from T to T?):
int? a = null;
int b = 2;
int? c = a + b; // c is null - equivalent to a + (int?)b
c.Dump();
And+Or operators
// When supplied operands of type bool?, the & and | operators treat null as an unknown
// value, rather like with SQL:
bool? n = null;
bool? f = false;
bool? t = true;
Console.WriteLine (n | n); // (null)
Console.WriteLine (n | f); // (null)
Console.WriteLine (n | t); // True
Console.WriteLine (n & n); // (null)
Console.WriteLine (n & f); // False
Console.WriteLine (n & t); // (null)
Null Coalescing Operator
// The ?? operator is the null coalescing operator, and it can be used with both
// nullable types and reference types. It says “If the operand is non-null, give
// it to me; otherwise, give me a default value.”:
int? x = null;
int y = x ?? 5;
Console.WriteLine (y); // 5
int? a = null, b = 1, c = 2;
Console.WriteLine (a ?? b ?? c); // 1 (first non-null value)
Null-Conditional Operator
// Nullable types also work well with the null-conditional operator (see “Null-Conditional Operator”)
System.Text.StringBuilder sb = null;
int? length = sb?.ToString().Length;
length.Dump();
// We can combine this with the null coalescing operator to evaluate to zero instead of null:
int length2 = sb?.ToString().Length ?? 0; // Evaluates to 0 if sb is null
length2.Dump();
Scenarios for Nullable Types
// Maps to a Customer table in a database
public class Customer
{
/*...*/
public decimal? AccountBalance; // Works well with SQL's nullable column
}
// Color is an ambient property:
public class Row
{
/*...*/
Grid parent;
Color? color;
public Color Color
{
get { return color ?? parent.Color; }
set { color = Color == parent.Color ? (Color?)null : value; }
}
}
class Grid
{
/*...*/
public Color Color { get; set; }
}
Nullable Reference Types
Null-Forgiving Operator
#nullable enable
// Enable nullable reference types
// This generates a warning:
void Foo1 (string? s) => Console.Write (s.Length);
// which we can remove with the null-forgiving operator:
void Foo2 (string? s) => Console.Write (s!.Length);
// If we add a check, we no longer need the null-forgiving operator in this case:
void Foo3 (string? s)
{
if (s != null) Console.Write (s.Length);
}
Nullable Reference Types
#nullable enable
string s1 = null; // Generates a compiler warning!
string? s2 = null; // OK: s2 is nullable reference type
class Foo
{
string x; // Generates a warning
}
Separating the Annotation and Warning Contexts
#nullable enable annotations // Enable just the nullable annotation context
// Because we've enabled the annotation context, s1 is non-nullable, and s2 is nullable:
public void Foo (string s1, string? s2)
{
// Our use of s2.Length doesn't generate a warning, however,
// because we've enabled just the annotation context:
Console.Write (s2.Length);
}
void Main()
{
// Now let's enable the warning context, too
#nullable enable warnings
// Notice that this now generates a warning:
Foo (null, null);
}
Extension Methods
Extension Methods
// Extension methods allow an existing type to be extended with new methods without altering
// the definition of the original type:
Console.WriteLine ("Perth".IsCapitalized());
// Equivalent to:
Console.WriteLine (StringHelper.IsCapitalized ("Perth"));
// Interfaces can be extended, too:
Console.WriteLine ("Seattle".First()); // S
public static class StringHelper
{
public static bool IsCapitalized (this string s)
{
if (string.IsNullOrEmpty (s)) return false;
return char.IsUpper (s [0]);
}
public static T First<T> (this IEnumerable<T> sequence)
{
foreach (T element in sequence)
return element;
throw new InvalidOperationException ("No elements!");
}
}
Extension Method Chaining
// Extension methods, like instance methods, provide a tidy way to chain functions:
string x = "sausage".Pluralize().Capitalize();
x.Dump();
// Equivalent to:
string y = StringHelper.Capitalize (StringHelper.Pluralize ("sausage"));
y.Dump();
// LINQPad's Dump method is an extension method:
"sausage".Pluralize().Capitalize().Dump();
public static class StringHelper
{
public static string Pluralize (this string s) => s + "s"; // Very naiive implementation!
public static string Capitalize (this string s) => s.ToUpper();
}
Extension Methods vs Instance Methods
// Any compatible instance method will always take precedence over an extension method:
new Test().Foo ("string"); // Instance method wins, as you'd expect
new Test().Foo (123); // Instance method still wins
public class Test
{
public void Foo (object x) { "Instance".Dump(); } // This method always wins
}
public static class Extensions
{
public static void Foo (this object t, int x) => "Extension".Dump();
}
Extension Methods vs Extension Methods
// The extension method with more specific arguments wins. Classes & structs are
// considered more specific than interfaces:
"Perth".IsCapitalized().Dump();
static class StringHelper
{
public static bool IsCapitalized (this string s)
{
"StringHelper.IsCapitalized".Dump();
return char.IsUpper (s[0]);
}
}
static class EnumerableHelper
{
public static bool IsCapitalized (this IEnumerable<char> s)
{
"Enumerable.IsCapitalized".Dump();
return char.IsUpper (s.First());
}
}
Extension Methods on Interfaces
// The extension method with more specific arguments wins. Classes & structs are
// considered more specific than interfaces:
string[] strings = { "a", "b", null, "c" };
foreach (string s in strings.StripNulls())
Console.WriteLine (s);
static class Test
{
public static IEnumerable<T> StripNulls<T> (this IEnumerable<T> seq)
{
foreach (T t in seq)
if (t != null)
yield return t;
}
}
Extension Methods Calling Another
Console.WriteLine ("FF".IsHexNumber()); // True
Console.WriteLine ("1A".NotHexNumber()); // False
static public class Ext
{
static public bool IsHexNumber (this string candidate)
{
return int.TryParse(candidate, NumberStyles.HexNumber, null, out int _);
}
static public bool NotHexNumber (this string candidate)
{
return !IsHexNumber (candidate);
}
}
Anonymous Types
Anonymous Types
// An anonymous type is a simple class created by the compiler on the fly to store a set of values
var dude = new { Name = "Bob", Age = 23 };
dude.Dump();
// The ToString() method is overloaded:
dude.ToString().Dump();
Anonymous Types - Omitting Identifiers
int Age = 23;
// The following:
{
var dude = new { Name = "Bob", Age, Age.ToString().Length };
dude.Dump();
}
// is shorthand for:
{
var dude = new { Name = "Bob", Age = Age, Length = Age.ToString().Length };
dude.Dump();
}
Anonymous Types - Identity
// Two anonymous type instances will have the same underlying type if their elements are
// same-typed and they’re declared within the same assembly
var a1 = new { X = 2, Y = 4 };
var a2 = new { X = 2, Y = 4 };
Console.WriteLine (a1.GetType() == a2.GetType()); // True
// Additionally, the Equals method is overridden to perform equality comparisons:
Console.WriteLine (a1 == a2); // False
Console.WriteLine (a1.Equals (a2)); // True
Anonymous Types - with keyword
var a1 = new { A = 1, B = 2, C = 3, D = 4, E = 5 };
var a2 = a1 with { E = 10 };
a2.Dump();
Tuples
Tuple literals
var bob = ("Bob", 23); // Allow compiler to infer the element types
Console.WriteLine (bob.Item1); // Bob
Console.WriteLine (bob.Item2); // 23
// Tuples are mutable value types:
var joe = bob; // joe is a *copy* of job
joe.Item1 = "Joe"; // Change joe’s Item1 from Bob to Joe
Console.WriteLine (bob); // (Bob, 23)
Console.WriteLine (joe); // (Joe, 23)
Tuple literals - specifying types
(string,int) bob = ("Bob", 23); // var is not compulsory with tuples!
bob.Item1.Dump();
bob.Item2.Dump();
Returning tuple from method
(string, int) person = GetPerson(); // Could use 'var' here if we want
Console.WriteLine (person.Item1); // Bob
Console.WriteLine (person.Item2); // 23
static (string,int) GetPerson() => ("Bob", 23);
Naming tuple elements
var tuple = (Name:"Bob", Age:23);
Console.WriteLine (tuple.Name); // Bob
Console.WriteLine (tuple.Age); // 23
Naming tuple elements - types
var person = GetPerson();
Console.WriteLine (person.Name); // Bob
Console.WriteLine (person.Age); // 23
static (string Name, int Age) GetPerson() => ("Bob", 23);
Tuple type compatibility
(string Name, int Age, char Sex) bob1 = ("Bob", 23, 'M');
(string Age, int Sex, char Name) bob2 = bob1; // No error!
// Our particular example leads to confusing results:
Console.WriteLine (bob2.Name); // M
Console.WriteLine (bob2.Age); // Bob
Console.WriteLine (bob2.Sex); // 23
Aliasing tuples
// From C#, you can leverage the using directive to define aliases for tuples:
using Point = (int, int);
// This feature also works with tuples that have named elements:
using Point1 = (int X, int Y); // Legal (but not necessarily *good*!)
Point1 p = (3, 4);
// We’ll see shortly how records offer a fully-typed solution with the same level of conciseness:
record Point2 (int X, int Y);
Tuple.Create
ValueTuple<string,int> bob1 = ValueTuple.Create ("Bob", 23);
(string, int) bob2 = ValueTuple.Create ("Bob", 23);
bob1.Dump();
bob2.Dump();
Deconstructing tuples
var bob = ("Bob", 23);
(string name, int age) = bob; // Deconstruct the bob tuple into
// separate variables (name and age).
Console.WriteLine (name);
Console.WriteLine (age);
Deconstructing tuples - method call
var (name, age, sex) = GetBob();
Console.WriteLine (name); // Bob
Console.WriteLine (age); // 23
Console.WriteLine (sex); // M
static (string, int, char) GetBob() => ( "Bob", 23, 'M');
Equality Comparison
var t1 = ("one", 1);
var t2 = ("one", 1);
Console.WriteLine (t1.Equals (t2)); // True
Extra - Tuple Order Comparison
var tuples = new[]
{
("B", 50),
("B", 40),
("A", 30),
("A", 20)
};
tuples.OrderBy (x => x).Dump ("They're all now in order!");
Records
Defining a record
new Point (2, 3).Dump();
// Run the line below to look at Point in ILSpy.
// Util.OpenILSpy (typeof (Point));
record Point
{
public Point (double x, double y) => (X, Y) = (x, y);
public double X { get; init; }
public double Y { get; init; }
}
Defining a record - struct
new Point (2, 3).Dump();
// Run the line below to look at Point in ILSpy.
// Util.OpenILSpy (typeof (Point));
record struct Point
{
public Point (double x, double y) => (X, Y) = (x, y);
public double X { get; init; }
public double Y { get; init; }
}
Parameter lists in records
new Point (2, 3).Dump();
// Run the line below to look at Point in ILSpy.
// Util.OpenILSpy (typeof (Point));
record Point (double X, double Y);
Parameter lists in records - subclassing
var point3d = new Point3D (2, 3, 4).Dump();
// Run the line below to look at Point in ILSpy.
// Util.OpenILSpy (typeof (Point));
record Point (double X, double Y);
record Point3D (double X, double Y, double Z) : Point (X, Y);
Non-destructive mutation and the 'with' keyword
Point p1 = new Point (3, 3);
Point p2 = p1 with { Y = 4 };
p2.Dump();
Test t1 = new Test (1, 2, 3, 4, 5, 6, 7, 8);
Test t2 = t1 with { A = 10, C = 30 };
t2.Dump();
record Point (double X, double Y);
record Test (int A, int B, int C, int D, int E, int F, int G, int H);
Writing your own copy constructor
Point p1 = new Point (3, 4);
Point p2 = p1 with { Y = 5 };
p2.Dump();
record Point (double X, double Y)
{
protected Point (Point original)
{
"Copying...".Dump();
this.X = original.X;
this.Y = original.Y;
}
}
Property validation in records
var p1 = new Point (2, 3).Dump ("p1"); // OK
try
{
var p2 = new Point (double.NaN, 3);
}
catch (ArgumentException ex)
{
ex.Dump ("Expected error");
}
try
{
var p3 = p1 with { X = double.NaN };
}
catch (ArgumentException ex)
{
ex.Dump ("Expected error");
}
record Point
{
// Notice that we assign x to the X property (and not the _x field):
public Point (double x, double y) => (X, Y) = (x, y);
double _x;
public double X
{
get => _x;
init
{
if (double.IsNaN (value))
throw new ArgumentException ("X Cannot be NaN");
_x = value;
}
}
public double Y { get; init; }
}
Record with simple calculated property
Point p1 = new Point (2, 3);
Console.WriteLine (p1.DistanceFromOrigin);
record Point (double X, double Y)
{
// This gets recomputes every time we call it.
public double DistanceFromOrigin => Math.Sqrt (X * X + Y * Y);
}
Record with read-only and calculated property
Point p1 = new Point (2, 3);
Console.WriteLine (p1.DistanceFromOrigin);
record Point
{
public double X { get; }
public double Y { get; }
public double DistanceFromOrigin { get; }
// We calculate DistanceFromOrigin just once.
// This works, but doesn't allow for non-destructive mutation.
public Point (double x, double y) =>
(X, Y, DistanceFromOrigin) = (x, y, Math.Sqrt (x * x + y * y));
}
Record with lazy calculated field
Point p1 = new Point (2, 3);
Console.WriteLine (p1.DistanceFromOrigin);
Point p2 = p1 with { Y = 4 };
Console.WriteLine (p2.DistanceFromOrigin);
// The best solution is to use lazy evaluation.
record Point
{
public Point (double x, double y) => (X, Y) = (x, y);
double _x, _y;
public double X { get => _x; init { _x = value; _distance = null; } }
public double Y { get => _y; init { _y = value; _distance = null; } }
double? _distance;
public double DistanceFromOrigin => _distance ??= Math.Sqrt (X * X + Y * Y);
}
Lazy calculated field - alternative
Point p1 = new Point (2, 3);
Console.WriteLine (p1.DistanceFromOrigin);
Point p2 = p1 with { Y = 4 };
Console.WriteLine (p2.DistanceFromOrigin);
// This also works, but is not quite as efficient if we had additional
// properties that weren't part of the calculation.
record Point (double X, double Y)
{
double? _distance;
public double DistanceFromOrigin => _distance ??= Math.Sqrt (X * X + Y * Y);
protected Point (Point other) => (X, Y) = other;
}
Primary constructors - readonly property
var student = new Student ("2021000477", "Bloggs", "Joe").Dump();
record Student (string ID, string LastName, string GivenName)
{
// ID in the property initializer refers to the primary constructor parameter:
public string ID { get; } = ID;
// ID in the field initializer refers to the primary constructor parameter:
public readonly int EnrolmentYear = int.Parse (ID.Substring (0, 4));
// We can't non-destructive mutate ID, so storing it in this field is safe.
}
Primary constructors and property validation
var p1 = new Person1 (null).Dump(); // Null check is bypassed.
try
{
var p2 = new Person2 (null).Dump(); // Null check succeeds (throws).
}
catch (ArgumentNullException ex)
{
ex.Dump ("Expected exception");
}
// Primary constructors don't play well when you need property validation:
record Person1 (string Name)
{
string _name = Name; // Assigns to *FIELD*
public string Name
{
get => _name;
init => _name = value ?? throw new ArgumentNullException ("Name");
}
}
// The easiest solution is usually to write the constructor yourself:
record Person2
{
public Person2 (string name) => Name = name; // Assigns to *PROPERTY*
string _name;
public string Name
{
get => _name;
init => _name = value ?? throw new ArgumentNullException ("Name");
}
}
Records and equality comparison
var p1 = new Point (1, 2);
var p2 = new Point (1, 2);
Console.WriteLine (p1.Equals (p2)); // True
Console.WriteLine (p1 == p2); // True
record Point (double X, double Y);
Record with customized equality comparison
Console.WriteLine (new Point1 (1, 2) == new Point1 (1, 2)); // False
Console.WriteLine (new Point2 (1, 2) == new Point2 (1, 2)); // True
record Point1 (double X, double Y)
{
static int _nextInstance;
double _someOtherField = _nextInstance++;
}
record Point2 (double X, double Y)
{
static int _nextInstance;
double _someOtherField = _nextInstance++;
public virtual bool Equals (Point2 other) =>
other != null && X == other.X && Y == other.Y;
public override int GetHashCode() => (X, Y).GetHashCode();
}
Patterns
var pattern
IsJanetOrJohn ("Janet").Dump();
IsJanetOrJohn ("john").Dump();
bool IsJanetOrJohn (string name) =>
name.ToUpper() is var upper && (upper == "JANET" || upper == "JOHN");
Constant pattern
Foo (3);
void Foo (object obj)
{
// C# won’t let you use the == operator, because obj is object.
// However, we can use ‘is’
if (obj is 3) Console.WriteLine ("three");
}
Relational patterns
int x = 150;
if (x is > 100) Console.WriteLine ("x is greater than 100");
GetWeightCategory (15).Dump();
GetWeightCategory (20).Dump();
GetWeightCategory (25).Dump();
string GetWeightCategory (decimal bmi) => bmi switch
{
< 18.5m => "underweight",
< 25m => "normal",
< 30m => "overweight",
_ => "obese"
};
Relational patterns with object type
object obj = 2m; // decimal
Console.WriteLine (obj is < 3m); // True
Console.WriteLine (obj is < 3); // False
Pattern combinators
IsJanetOrJohn ("Janet").Dump();
IsVowel ('e').Dump();
Between1And9 (5).Dump();
IsLetter ('!').Dump();
bool IsJanetOrJohn (string name) => name.ToUpper() is "JANET" or "JOHN";
bool IsVowel (char c) => c is 'a' or 'e' or 'i' or 'o' or 'u';
bool Between1And9 (int n) => n is >= 1 and <= 9;
bool IsLetter (char c) => c is >= 'a' and <= 'z'
or >= 'A' and <= 'Z';
Not pattern
object obj = true;
if (obj is not string)
"obj is not a string".Dump();
Tuple patterns
AverageCelsiusTemperature (Season.Spring, true).Dump();
int AverageCelsiusTemperature (Season season, bool daytime) =>
(season, daytime) switch
{
(Season.Spring, true) => 20,
(Season.Spring, false) => 16,
(Season.Summer, true) => 27,
(Season.Summer, false) => 22,
(Season.Fall, true) => 18,
(Season.Fall, false) => 12,
(Season.Winter, true) => 10,
(Season.Winter, false) => -2,
_ => throw new Exception ("Unexpected combination")
};
enum Season { Spring, Summer, Fall, Winter };
Positional pattern
var p = new Point (2, 2);
Console.WriteLine (p is (2, 2)); // True
Console.WriteLine (p is (var x, var y) && x == y); // True
Print (new Point (0, 0)).Dump();
Print (new Point (1, 1)).Dump();
string Print (object obj) => obj switch
{
Point (0, 0) => "Empty point",
Point (var x, var y) when x == y => "Diagonal",
_ => "Other"
};
record Point (int X, int Y);
Positional pattern - with class
Print (new Point (0, 0)).Dump();
Print (new Point (1, 1)).Dump();
string Print (object obj) => obj switch
{
Point (0, 0) => "Empty point",
Point (var x, var y) when x == y => "Diagonal",
_ => "Other"
};
class Point
{
public readonly int X, Y;
public Point (int x, int y) => (X, Y) = (x, y);
// Here's our deconstructor:
public void Deconstruct (out int x, out int y)
{
x = X; y = Y;
}
}
Property pattern with is operator
object obj = "test";
if (obj is string { Length:4 })
Console.WriteLine ("string with length of 4");
Property pattern with switch
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (Uri uri) => uri switch
{
{ Scheme: "http", Port: 80 } => true,
{ Scheme: "https", Port: 443 } => true,
{ Scheme: "ftp", Port: 21 } => true,
{ IsLoopback: true } => true,
_ => false
};
Property pattern with switch - nested
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (Uri uri) => uri switch
{
{ Scheme: { Length: 4 }, Port: 80 } => true,
_ => false
};
Property pattern with switch - nested simplified
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (Uri uri) => uri switch
{
{ Scheme.Length: 4, Port: 80 } => true, // Simplified syntax (from C# 10)
_ => false
};
Property pattern with relational pattern
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (Uri uri) => uri switch
{
{ Host: { Length: < 1000 }, Port: > 0 } => true,
_ => false
};
Property pattern with switch - with when clause
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (Uri uri) => uri switch
{
{ Scheme: "http" } when string.IsNullOrWhiteSpace (uri.Query) => true,
_ => false
};
Property pattern with type pattern
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (object uri) => uri switch
{
Uri { Scheme: "http", Port: 80 } httpUri => httpUri.Host.Length < 1000,
Uri { Scheme: "https", Port: 443 } => true,
Uri { Scheme: "ftp", Port: 21 } => true,
Uri { IsLoopback: true } => true,
_ => false
};
Property pattern with type pattern and when
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (object uri) => uri switch
{
Uri { Scheme: "http", Port: 80 } httpUri
when httpUri.Host.Length < 1000 => true,
Uri { Scheme: "https", Port: 443 } => true,
Uri { Scheme: "ftp", Port: 21 } => true,
Uri { IsLoopback: true } => true,
_ => false
};
Property pattern with property variable
Console.WriteLine (ShouldAllow (new Uri ("http://www.linqpad.net")));
Console.WriteLine (ShouldAllow (new Uri ("ftp://ftp.microsoft.com")));
Console.WriteLine (ShouldAllow (new Uri ("tcp:foo.database.windows.net")));
bool ShouldAllow (Uri uri) => uri switch
{
{ Scheme: "http", Port: 80, Host: var host } => host.Length < 1000,
{ Scheme: "https", Port: 443 } => true,
{ Scheme: "ftp", Port: 21 } => true,
{ IsLoopback: true } => true,
_ => false
};
List patterns
// List patterns (from C# 11) work with any collection type that is countable
// (with a Count or Length property) and indexable (with an indexer of type int or System.Index).
// A list pattern matches a series of elements in square brackets:
int[] numbers = { 0, 1, 2, 3, 4 };
Console.Write (numbers is [0, 1, 2, 3, 4]); // True
// An underscore matches a single element of any value:
Console.Write (numbers is [0, 1, _, _, 4]); // True
// The var pattern also works in matching a single element:
Console.Write (numbers is [0, 1, var x, 3, 4] && x > 1); // True
// Two dots indicate a slice.A slice matches zero or more elements:
Console.Write (numbers is [0, .., 4]); // True
// With arrays and other types that support indices and ranges
// - see query://../../Chapter_2_-_C#_Language_Basics/Arrays/Indices
// and query://../../Chapter_2_-_C#_Language_Basics/Arrays/Ranges
// you can follow a slice with a var pattern:
Console.Write (numbers is [0, .. var mid, 4] && mid.Contains (2)); // True
// A list pattern can include at most one slice.
Attributes (see also CH19)
Attaching Attributes
new Foo(); // Generates a warning because Foo is obsolete
[Obsolete]
public class Foo
{
}
Named and Positional Attribute Parameters
void Main()
{
}
[XmlType ("Customer", Namespace = "http://oreilly.com")]
public class CustomerEntity
{
}
Applying Attributes to Assemblies and Backing Fields
[assembly: AssemblyFileVersion ("1.2.3.4")]
class Foo
{
[field: NonSerialized]
public int MyProperty { get; set; }
}
void Main()
{
}
Applying Attributes to lambda expressions
Action<int> a = [Description ("Method")]
[return: Description ("Return value")]
([Description ("Parameter")]int x) => Console.WriteLine (x);
a.GetMethodInfo().GetCustomAttributes().Dump();
a.GetMethodInfo().GetParameters() [0].GetCustomAttributes().Dump();
a.GetMethodInfo().ReturnParameter.GetCustomAttributes().Dump();
Specifying Multiple Attributes
[assembly:CLSCompliant(false)]
[Serializable, Obsolete, CLSCompliant (false)]
public class Bar1 {}
[Serializable]
[Obsolete]
[CLSCompliant (false)]
public class Bar2 {}
[Serializable, Obsolete]
[CLSCompliant (false)]
public class Bar3 {}
Caller Info Attributes
static void Main() => Foo();
static void Foo (
[CallerMemberName] string memberName = null,
[CallerFilePath] string filePath = null,
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine (memberName);
Console.WriteLine (filePath);
Console.WriteLine (lineNumber);
}
Caller Info Attributes - INotifyPropertyChanged
var foo = new Foo();
foo.PropertyChanged += (sender, args) => args.Dump ("Property changed!");
foo.CustomerName = "asdf";
public class Foo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
void RaisePropertyChanged ([CallerMemberName] string propertyName = null)
=> PropertyChanged (this, new PropertyChangedEventArgs (propertyName));
string customerName;
public string CustomerName
{
get => customerName;
set
{
if (value == customerName) return;
customerName = value;
RaisePropertyChanged();
// The compiler converts the above line to:
// RaisePropertyChanged ("CustomerName");
}
}
}
CallerArgumentExpression
Print (Math.PI * 2);
Print (Math.PI /*(π)*/ * 2);
void Print (double number,
[CallerArgumentExpression("number")] string expr = null)
=> Console.WriteLine (expr);
Assert (2 + 2 == 5);
void Assert (bool condition,
[CallerArgumentExpression ("condition")] string message = null)
{
if (!condition) throw new Exception ("Assertion failed: " + message);
}
Dynamic Binding (see also CH20)
Custom Binding
// Custom binding occurs when a dynamic object implements IDynamicMetaObjectProvider:
dynamic d = new Duck();
d.Quack(); // Quack method was called
d.Waddle(); // Waddle method was called
public class Duck : System.Dynamic.DynamicObject
{
public override bool TryInvokeMember (
InvokeMemberBinder binder, object[] args, out object result)
{
Console.WriteLine (binder.Name + " method was called");
result = null;
return true;
}
}
Language Binding
// Language binding occurs when a dynamic object does not implement IDynamicMetaObjectProvider:
int x = 3, y = 4;
Console.WriteLine (Mean (x, y));
static dynamic Mean (dynamic x, dynamic y) => (x + y) / 2;
RuntimeBinderException
// If a member fails to bind, a RuntimeBinderException is thrown. This can be
// thought of like a compile-time error at runtime:
dynamic d = 5;
d.Hello(); // throws RuntimeBinderException
Runtime Representation of Dynamic
// The following expression is true, although the compiler does not permit it:
// typeof (dynamic) == typeof (object)
// This principle extends to constructed types and array types:
(typeof (List<dynamic>) == typeof (List<object>)).Dump(); // True
(typeof (dynamic[]) == typeof (object[])).Dump(); // True
// Like an object reference, a dynamic reference can point to an object of any type (except pointer types):
dynamic x = "hello";
Console.WriteLine (x.GetType().Name); // String
x = 123; // No error (despite same variable)
Console.WriteLine (x.GetType().Name); // Int32
// You can convert from object to dynamic to perform any dynamic operation you want on an object:
object o = new System.Text.StringBuilder();
dynamic d = o;
d.Append ("hello");
Console.WriteLine (o); // hello
Dynamic Conversions
// The dynamic type has implicit conversions to and from all other types:
int i = 7;
dynamic d = i;
int j = d; // Implicit conversion (or more precisely, an *assignment conversion*)
j.Dump();
// The following throws a RuntimeBinderException because an int is not implicitly convertible to a short:
short s = d;
var vs dynamic
// var says, “let the compiler figure out the type”.
// dynamic says, “let the runtime figure out the type”.
dynamic x = "hello"; // Static type is dynamic, runtime type is string
var y = "hello"; // Static type is string, runtime type is string
int i = x; // Run-time error
int j = y; // Compile-time error
Static type of var can be dynamic
// The static type of a variable declared of type var can be dynamic:
dynamic x = "hello";
var y = x; // Static type of y is dynamic
int z = y; // Run-time error
Dynamic Expressions
// Trying to consume the result of a dynamic expression with a void return type is
// prohibited — just as with a statically typed expression. However, the error occurs at runtime:
dynamic list = new List<int>();
var result = list.Add (5); // RuntimeBinderException thrown
// Expressions involving dynamic operands are typically themselves dynamic:
dynamic x = 2;
var y = x * 3; // Static type of y is dynamic
// However, casting a dynamic expression to a static type yields a static expression:
dynamic a = 2;
var b = (int)2; // Static type of b is int
// And constructor invocations always yield static expressions:
dynamic capacity = 10;
var sb = new System.Text.StringBuilder (capacity);
int len = sb.Length;
Dynamic Calls without Dynamic Receivers
// You can also call statically known functions with dynamic arguments.
// Such calls are subject to dynamic overload resolution:
static void Foo (int x) { Console.WriteLine ("1"); }
static void Foo (string x) { Console.WriteLine ("2"); }
static void Main()
{
dynamic x = 5;
dynamic y = "watermelon";
Foo (x); // 1
Foo (y); // 2
}
Static Types in Dynamic Expressions
// Static types are also used — wherever possible — in dynamic binding:
// Note: the following sometimes throws a RuntimeBinderException in Framework 4.0 beta 2. This is a bug.
static void Foo (object x, object y) { Console.WriteLine ("oo"); }
static void Foo (object x, string y) { Console.WriteLine ("os"); }
static void Foo (string x, object y) { Console.WriteLine ("so"); }
static void Foo (string x, string y) { Console.WriteLine ("ss"); }
static void Main()
{
object o = "hello";
dynamic d = "goodbye";
Foo (o, d); // os
}
Uncallable Functions
// You cannot dynamically call:
// • Extension methods (via extension method syntax)
// • Any member of an interface
// • Base members hidden by a subclass
IFoo f = new Foo();
dynamic d = f;
try
{
d.Test(); // Exception thrown
}
catch (RuntimeBinderException error)
{
error.Dump ("You cannot call explicit interface members via dynamic binding");
}
// A workaround is to use the Uncapsulator library (available on NuGet, and built into LINQPad)
dynamic u = f.Uncapsulate();
u.Test();
interface IFoo { void Test(); }
class Foo : IFoo { void IFoo.Test() => "Test".Dump(); }
Operator Overloading (see also CH6)
Operator Functions
// An operator is overloaded by declaring an operator function:
Note B = new Note (2);
Note CSharp = B + 2;
CSharp.SemitonesFromA.Dump();
CSharp += 2;
CSharp.SemitonesFromA.Dump();
public struct Note
{
int value;
public int SemitonesFromA => value;
public Note (int semitonesFromA) { value = semitonesFromA; }
public static Note operator + (Note x, int semitones)
=> new Note (x.value + semitones);
// See the last example in "Equality Comparison", Chapter 6 for an example of overloading the == operator
}
Operator Functions - checked
// From C# 11, when you declare an operator function, you can also declare a checked version.
// The checked version will be called inside checked expressions or blocks.
Note B = new Note (2);
Note other = checked (B + int.MaxValue); // throws OverflowException
public struct Note
{
int value;
public int SemitonesFromA => value;
public Note (int semitonesFromA) { value = semitonesFromA; }
public static Note operator + (Note x, int semitones)
=> new Note (x.value + semitones);
public static Note operator checked + (Note x, int semitones)
=> checked (new Note (x.value + semitones));
}
Custom Implicit and Explicit Conversions
// Implicit and explicit conversions are overloadable operators:
Note n = (Note)554.37; // explicit conversion
double x = n; // implicit conversion
x.Dump();
public struct Note
{
int value;
public int SemitonesFromA { get { return value; } }
public Note (int semitonesFromA) { value = semitonesFromA; }
// Convert to hertz
public static implicit operator double (Note x) => 440 * Math.Pow (2, (double)x.value / 12);
// Convert from hertz (accurate to the nearest semitone)
public static explicit operator Note (double x) =>
new Note ((int)(0.5 + 12 * (Math.Log (x / 440) / Math.Log (2))));
}
Overloading true and false
// The true and false operators are overloaded in the extremely rare case of types that
// are boolean “in spirit”, but do not have a conversion to bool.
// An example is the System.Data.SqlTypes.SqlBoolean type which is defined as follows:
SqlBoolean a = SqlBoolean.Null;
if (a)
Console.WriteLine ("True");
else if (!a)
Console.WriteLine ("False");
else
Console.WriteLine ("Null");
public struct SqlBoolean
{
public static bool operator true (SqlBoolean x) => x.m_value == True.m_value;
public static bool operator false (SqlBoolean x) => x.m_value == False.m_value;
public static SqlBoolean operator ! (SqlBoolean x)
{
if (x.m_value == Null.m_value) return Null;
if (x.m_value == False.m_value) return True;
return False;
}
public static readonly SqlBoolean Null = new SqlBoolean (0);
public static readonly SqlBoolean False = new SqlBoolean (1);
public static readonly SqlBoolean True = new SqlBoolean (2);
SqlBoolean (byte value) { m_value = value; }
byte m_value;
}
Static Polymorphism
Static Polymorphism - example
// Consider the following interface, which defines a static method to create a random instance of some type T:
interface ICreateRandom<T>
{
static abstract T CreateRandom(); // Create a random instance of T
}
// Let's implement that interface:
record Point (int X, int Y) : ICreateRandom<Point>
{
static Random rnd = new();
public static Point CreateRandom() => new Point (rnd.Next(), rnd.Next());
}
void Main()
{
// To call this method via the interface, we use a constrained type parameter.
// The following method creates an array of test data using this approach:
T[] CreateTestData<T> (int count) where T : ICreateRandom<T>
{
T[] result = new T [count];
for (int i = 0; i < count; i++)
result [i] = T.CreateRandom();
return result;
}
Point[] testData = CreateTestData<Point> (50); // Create 50 random Points.
testData.Dump();
// Our call to the static CreateRandom method in CreateTestData is polymorphic because it works
// not just with Point, but with any type that implements ICreateRandom<T>. This is different to
// instance polymorphism, because we don’t need an instance of ICreateRandom<T> on which to call
// CreateRandom; we call CreateRandom on the type itself.
}
Polymorphic operators
// Because operators are essentially static functions (see “Operator Overloading”),
// operators can also be declared as static virtual interface members:
interface IAddable<T> where T : IAddable<T>
{
abstract static T operator + (T left, T right);
}
// Here’s how we can implement the interface:
record Point (int X, int Y) : IAddable<Point>
{
public static Point operator + (Point left, Point right) =>
new Point (left.X + right.X, left.Y + right.Y);
}
void Main()
{
// With a constrained type parameter, we can then write a method that calls our
// addition operator polymorphically (with edge-case handling omitted for brevity):
T Sum<T> (params T[] values) where T : IAddable<T>
{
T total = values [0];
for (int i = 1; i < values.Length; i++)
total += values [i];
return total;
}
Sum (new Point (1, 2), new Point (3, 4)).Dump();
//Our call to the + operator (via the += operator) is polymorphic because it binds to
// IAddable<T>, not Point. Hence, our Sum method works with all types that implement IAddable<T>.
}
Generic Math
// .NET 7 introduced the INumber<TSelf> interface to unify arithmetic operations across
// numeric types, allowing generic methods such as the following to be written:
T Sum<T> (params T[] numbers) where T : System.Numerics.INumber<T>
{
T total = T.Zero;
foreach (T n in numbers)
total += n; // Invokes addition operator for any numeric type
return total;
}
int intSum = Sum (3, 5, 7);
double doubleSum = Sum (3.2, 5.3, 7.1);
decimal decimalSum = Sum (3.2m, 5.3m, 7.1m);
// The System.Numerics namespace also contains interfaces that are not part of INumber for
// operations specific to certain kinds of numbers (such as floating-point). To compute a
// root-mean-square, for instance, we can add the IRootFunctions<T> interface to the
// constraint list to expose its static RootN method to T:
T RMS<T> (params T[] values) where T : System.Numerics.INumber<T>, System.Numerics.IRootFunctions<T>
{
T total = T.Zero;
for (int i = 0; i < values.Length; i++)
total += values [i] * values [i];
// Use T.CreateChecked to convert values.Length (type int) to T.
T count = T.CreateChecked (values.Length);
return T.RootN (total / count, 2); // Calculate square root
}
float rms1 = RMS (5f, 10f, 20f).Dump();
double rms2 = RMS (5d, 10d, 20d).Dump();
Unsafe Code and Pointers (see also CH25)
Unsafe Code
// C# supports direct memory manipulation via pointers within blocks of code marked unsafe
// and compiled with the /unsafe compiler option. LINQPad implicitly compiles with this option.
// Here's how to use pointers to quickly process a bitmap:
int [,] bitmap = { { 0x101010, 0x808080, 0xFFFFFF }, { 0x101010, 0x808080, 0xFFFFFF } };
BlueFilter (bitmap);
bitmap.Dump();
unsafe static void BlueFilter (int [,] bitmap)
{
int length = bitmap.Length;
fixed (int* b = bitmap)
{
int* p = b;
for (int i = 0; i < length; i++)
*p++ &= 0xFF;
}
}
Pinning variables with fixed
// Value types declared inline within reference types require the reference type to be pinned:
Test test = new Test();
unsafe
{
fixed (int* p = &test.X) // Pins test
{
*p = 9;
}
Console.WriteLine (test.X);
}
class Test
{
public int X;
}
The Pointer-to-Member Operator
// In addition to the & and * operators, C# also provides the C++ style -> operator,
// which can be used on structs:
Test test = new Test();
Test * p = &test;
p->X = 9;
Console.WriteLine (test.X);
struct Test
{
public int X;
}
The stackalloc keyword
// Memory can be allocated in a block on the stack explicitly using the stackalloc keyword:
unsafe
{
int* a = stackalloc int [10];
for (int i = 0; i < 10; ++i)
Console.WriteLine (a[i]); // Print raw memory
}
Fixed-Size Buffers
// Memory can be allocated in a block within a struct using the fixed keyword:
new UnsafeClass ("Christian Troy");
unsafe struct UnsafeUnicodeString
{
public short Length;
public fixed byte Buffer[30];
}
unsafe class UnsafeClass
{
UnsafeUnicodeString uus;
public UnsafeClass (string s)
{
uus.Length = (short)s.Length;
fixed (byte* p = uus.Buffer)
for (int i = 0; i < s.Length; i++)
p[i] = (byte) s[i];
}
}
void-star
// A void pointer (void*) makes no assumptions about the type of the underlying data and is
// useful for functions that deal with raw memory:
short[] a = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
unsafe
{
fixed (short* p = a)
{
//sizeof returns size of value-type in bytes
Zap (p, a.Length * sizeof (short));
}
}
foreach (short x in a)
System.Console.WriteLine (x); // Prints all zeros
unsafe void Zap (void* memory, int byteCount)
{
byte* b = (byte*)memory;
for (int i = 0; i < byteCount; i++)
*b++ = 0;
}
Native-sized integers
// The nint and unint native-sized integer types are sized to match the address space
// of the processor and operating system at runtime (in practice, 32 or 64 bits).
// Native-sized integers behave much like standard integers, with support
// for arithmetic operations, overflow check operators, and so on:
nint x = 123;
nint y = 234;
checked
{
nint sum = x + y, product = x * y;
product.Dump();
}
// When working with pointers, native-sized integers can improve efficiency because the
// result of subtracting two pointers in C# is always a 64-bit integer (long), which is
// inefficient on 32-bit platforms. By first casting the pointers to nint, the result
// of a subtraction is also nint (which will be 32 bits on a 32-bit platform):
unsafe nint AddressDif (char* x, char* y) => (nint)x - (nint)y;
// When targeting .NET 7 or later, nint and nuint act as synonyms for the underlying
// .NET types System.IntPtr and System.UIntPtr.
// When .NET 6 or below (or.NET Standard), nint and nuint still use IntPtr and UIntPtr
// as their underlying types. However, because the legacy IntPtr and UIntPtr types lack
// support for most arithmetic operations, the compiler fills in the gaps, making the
// nint/nuint types behave as they would in .NET 7+ (including allowing checked operations).
nint native = 123;
Console.WriteLine (native * native); // Works on .NET 6 (as well as later versions)
IntPtr ip = native;
Console.WriteLine (ip * ip); // Requires .NET 7+
Native-sized integers - MemCopy
const int arrayLength = 16;
byte[] array1 = Enumerable.Range (0, arrayLength).Select (x => (byte)x).ToArray();
byte[] array2 = new byte[arrayLength];
fixed (byte* p1 = array1)
fixed (byte* p2 = array2)
{
// Our simplified version of MemCopy:
MemCopy (p1, p2, arrayLength);
array2.Dump();
// To see a real-world implementation of MemCopy, hit F12 on Buffer.MemoryCopy:
Buffer.MemoryCopy (p1, p2, arrayLength, arrayLength);
array2.Dump();
}
// Simplified implementation (assumes nicely aligned boundaries, numberOfBytes divisible by nint size)
void MemCopy (void* source, void* dest, nuint numberOfBytes)
{
var nativeSource = (nint*)source;
var nativeDest = (nint*)dest;
// We want to copy memory one native word-length at a time - this is likely to be most efficient.
// Hence we need to calculate the number of iterations = numberOfBytes / word-length.
// nuint is the ideal type for this calculation.
nuint iterations = numberOfBytes / (nuint) sizeof (nuint);
// Because nativeSource and nativeDest are nint*, we will enumerate the memory in increments
// of the native word size, which is exactly our goal.
for (nuint i = 0; i < iterations; i++)
nativeDest[i] = nativeSource[i];
}
Function Pointers
delegate*<string, int> functionPointer = &GetLength;
int length = functionPointer ("Hello, world");
length.Dump();
((IntPtr)functionPointer).Dump ("Address in memory");
// Don't try this at home!
var pointer2 = (delegate*<string, decimal>)(IntPtr)functionPointer;
pointer2 ("Hello, unsafe world").Dump ("Some random memory!");
static int GetLength (string s) => s.Length;
[SkipLocalsInit]
void Main()
{
for (int i = 0; i < 1000; i++)
Foo();
}
// Uncomment the following attribute to disable automatic initialization of local variables
//[SkipLocalsInit]
unsafe void Foo()
{
int local;
int* ptr = &local;
Console.Write (*ptr + " ");
int* a = stackalloc int [10];
for (int i = 0; i < 10; ++i) Console.Write (a [i] + " ");
Console.WriteLine ();
}