Chapter 4 - Advanced C#
Delegates
Delegates
// A delegate type declaration is like an abstract method declaration, prefixed with the delegate keyword: delegate int Transformer (int x); static void Main() { // 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 } static int Square (int x) => x * x;
Delegates - longhand
delegate int Transformer (int x); static void Main() { Transformer t = new Transformer (Square); // Create delegate instance int result = t.Invoke (3); // Invoke delegate Console.WriteLine (result); // 9 } static int Square (int x) => x * x;
Delegates - Writing Plug-in Methods
// A delegate variable is assigned a method dynamically. This is useful for writing plug-in methods: delegate int Transformer (int x); class Util { public static void Transform (int[] values, Transformer t) { for (int i = 0; i < values.Length; i++) values[i] = t (values[i]); } } static void Main() { int[] values = { 1, 2, 3 }; Util.Transform (values, Square); // Hook in the Square method values.Dump(); values = new int[] { 1, 2, 3 }; Util.Transform (values, Cube); // Hook in the Cube method values.Dump(); } static int Square (int x) => x * x; static int Cube (int x) => x * x * x;
Multicast Delegates
// All delegate instances have multicast capability: delegate void SomeDelegate(); static void Main() { SomeDelegate d = SomeMethod1; d += SomeMethod2; d(); " -- SomeMethod1 and SomeMethod2 both fired\r\n".Dump(); d -= SomeMethod1; d(); " -- Only SomeMethod2 fired".Dump(); } static void SomeMethod1 () { "SomeMethod1".Dump(); } static void SomeMethod2 () { "SomeMethod2".Dump(); }
Multicast Delegates - ProgressReporter
public delegate void ProgressReporter (int percentComplete); public 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 } } } static void Main() { ProgressReporter p = WriteProgressToConsole; p += WriteProgressToFile; Util.HardWork (p); } static void WriteProgressToConsole (int percentComplete) { Console.WriteLine (percentComplete); } static void WriteProgressToFile (int percentComplete) { System.IO.File.WriteAllText ("progress.txt", percentComplete.ToString()); }
Instance vs Static Methods
// 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: public delegate void ProgressReporter (int percentComplete); static void Main() { X x = new X(); ProgressReporter p = x.InstanceProgress; p(99); // 99 Console.WriteLine (p.Target == x); // True Console.WriteLine (p.Method); // Void InstanceProgress(Int32) } class X { public void InstanceProgress (int percentComplete) => Console.WriteLine (percentComplete); }
Generic Delegate Types
// A delegate type may contain generic type parameters: 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]); } } static void Main() { int[] values = { 1, 2, 3 }; Util.Transform (values, Square); // Dynamically hook in Square values.Dump(); } static int Square (int x) => x * x;
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: 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]); } } static void Main() { int[] values = { 1, 2, 3 }; Util.Transform (values, Square); // Dynamically hook in Square values.Dump(); } static int Square (int x) => x * x;
Delegates vs Interfaces
// A problem that can be solved with a delegate can also be solved with an interface: 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; } public static void Main() { int[] values = { 1, 2, 3 }; Util.TransformAll (values, new Squarer()); values.Dump(); }
Delegates vs Interfaces - Clumsiness
// With interfaces, we’re forced into writing a separate type per transform // since Test can only implement ITransformer once: 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; } static void Main() { int[] values = { 1, 2, 3 }; Util.TransformAll (values, new Cuber()); values.Dump(); }
Delegate Type Incompatibility
// Delegate types are all incompatible with each other, even if their signatures are the same: delegate void D1(); delegate void D2(); static void Main() { D1 d1 = Method1; D2 d2 = d1; // Compile-time error } static void Method1() { }
Delegate Type Incompatibility - Workaround
// Delegate types are all incompatible with each other, even if their signatures are the same: delegate void D1(); delegate void D2(); static void Main() { D1 d1 = Method1; D2 d2 = new D2 (d1); // Legal } static void Method1() { }
Delegate Equality
// Delegate instances are considered equal if they have the same method targets: delegate void D(); static void Main() { D d1 = Method1; D d2 = Method1; Console.WriteLine (d1 == d2); // True } static void Method1() { }
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: delegate object ObjectRetriever(); static void Main() { ObjectRetriever o = new ObjectRetriever (RetriveString); object result = o(); Console.WriteLine (result); // hello } static string RetriveString() => "hello";
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. 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. } } } static void Main() { var stock = new Stock ("MSFT"); stock.PriceChanged += ReportPriceChange; stock.Price = 123; stock.Price = 456; } static void ReportPriceChange (decimal oldPrice, decimal newPrice) { ("Price changed from " + oldPrice + " to " + newPrice).Dump(); }
Standard Event Pattern
// The .NET Framework defines a standard pattern for writing events. The pattern provides // consistency across both Framework and user code. 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)); } } } static void Main() { Stock stock = new Stock ("THPW"); stock.Price = 27.10M; // Register with the PriceChanged event stock.PriceChanged += stock_PriceChanged; stock.Price = 31.59M; } static void stock_PriceChanged (object sender, PriceChangedEventArgs e) { if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M) Console.WriteLine ("Alert, 10% stock price increase!"); }
Standard Event Pattern - Simple EventHandler
// The predefined nongeneric EventHandler delegate can be used when an event doesn't // carry extra information: 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); } } } static void Main() { Stock stock = new Stock ("THPW"); stock.Price = 27.10M; // Register with the PriceChanged event stock.PriceChanged += stock_PriceChanged; stock.Price = 31.59M; } static void stock_PriceChanged (object sender, EventArgs e) { Console.WriteLine ("New price = " + ((Stock) sender).Price); }
Event Accessors
// We can take over the default event implementation by writing our own accessors: 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); } } } static void Main() { Stock stock = new Stock ("THPW"); stock.Price = 27.10M; // Register with the PriceChanged event stock.PriceChanged += stock_PriceChanged; stock.Price = 31.59M; } static void stock_PriceChanged (object sender, EventArgs e) { Console.WriteLine ("New price = " + ((Stock) sender).Price); }
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; } } } static void Main() { }
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 delegate int Transformer (int i); static void Main() { 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)); // 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)); }
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 }
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: static int Calc (int x) { return 10 / x; } static void Main() { int y = Calc (0); Console.WriteLine (y); }
DivideByZeroException handled
// We can catch the DivideByZeroException as follows: static int Calc (int x) { return 10 / x; } static void Main() { try { int y = Calc (0); Console.WriteLine (y); } catch (DivideByZeroException ex) { Console.WriteLine ("x cannot be zero"); } Console.WriteLine ("program completed"); }
The catch Clause
// You can handle multiple exception types with multiple catch clauses: static void Main() { Main ("one"); } static void Main (params string[] args) { try { byte b = byte.Parse (args[0]); Console.WriteLine (b); } catch (IndexOutOfRangeException ex) { Console.WriteLine ("Please provide at least one argument"); } catch (FormatException ex) { Console.WriteLine ("That's not a number!"); } catch (OverflowException ex) { Console.WriteLine ("You've given me more than a byte!"); } } static int Calc (int x) { return 10 / x; }
Exception Filters
try { new WebClient().DownloadString ("http://thisDoesNotExist"); } catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout) { "Timeout!".Dump(); } catch (WebException ex) when (ex.Status == WebExceptionStatus.NameResolutionFailure) { "Name resolution failure!".Dump(); } catch (WebException ex) { $"Some other failure: {ex.Status}".Dump(); }
The finally Block
// finally blocks are typically used for cleanup code: static 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(); } } static void Main() { File.WriteAllText ("file.txt", "test"); ReadFile (); }
The using Statement
// The using statement provides an elegant syntax for calling Dispose on // an IDisposable object within a finally block: static void ReadFile() { using (StreamReader reader = File.OpenText ("file.txt")) { if (reader.EndOfStream) return; Console.WriteLine (reader.ReadToEnd()); } } static void Main() { File.WriteAllText ("file.txt", "test"); ReadFile (); }
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: static void Display (string name) { if (name == null) throw new ArgumentNullException (nameof (name)); Console.WriteLine (name); } static void Main() { try { Display (null); } catch (ArgumentNullException ex) { Console.WriteLine ("Caught the exception"); } }
throw Expressions
// Prior to C# 7, throw was always a statement. Now it can also appear as an expression in // expression-bodied functions: public 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); void Main() { ProperCase ("test").Dump(); ProperCase (null).Dump(); // throws an ArgumentException }
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; using (WebClient wc = new WebClient()) try { s = wc.DownloadString ("http://www.albahari.com/nutshell/"); } catch (WebException ex) { if (ex.Status == WebExceptionStatus.NameResolutionFailure) Console.WriteLine ("Bad domain name"); else throw; // Can’t handle other sorts of WebException, 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); } catch (FormatException ex) { throw new XmlException ("Invalid DateTime", ex); }
The TryXXX Pattern
static void Main() { bool result; TryToBoolean ("Bad", out result).Dump ("Successful"); result = ToBoolean ("Bad"); // throws Exception } public static bool ToBoolean (string text) { bool returnValue; if (!TryToBoolean (text, out returnValue)) throw new FormatException ("Cannot parse to Boolean"); return returnValue; } public static 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
static void Main() { 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 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: static void Main() { foreach (int fib in Fibs(6)) Console.Write (fib + " "); } static 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: static void Main() { 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: static void Main() { foreach (string s in Foo()) Console.WriteLine(s); // Prints "One","Two","Three" } static 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: static void Main() { foreach (string s in Foo()) Console.WriteLine(s); } static 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: static void Main() { foreach (string s in Foo()) s.Dump(); Console.WriteLine(); foreach (string s in Foo()) { ("First element is " + s).Dump(); break; } } static IEnumerable<string> Foo() { try { yield return "One"; yield return "Two"; yield return "Three"; } finally { "Finally".Dump(); } }
Composing Iterators
// Iterators are highly composable: static void Main() { foreach (int fib in EvenNumbersOnly (Fibs(6))) Console.WriteLine (fib); } static 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; } } static 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; } } void Main() { }
Nullable Reference Types
#nullable enable void Main() { // Number and Photo must be assigned non-null values because we're // in an enabled nullable annotation context. var myLicense = new DriversLicense() { Number = "C12345", Photo = new Bitmap (512, 512) // A real system might load from a database or camera attached to the computer }; // Both of these can safely be derenced: Console.WriteLine (myLicense.Number.Length); Console.WriteLine (myLicense.Photo.Height); // This violates CS8602 and will generate either a warning or error // depending on whether nullable annotation context related warnings // are configured to produce errors instead: Console.WriteLine (myLicense.Restrictions.Length); // By using the null forgiveness operator (!), we tell the compiler to // ignore its static flow analysis. We state we know better (and if // we're wrong, a NullReferenceException results). Console.WriteLine (myLicense.Restrictions!.Length); myLicense.Restrictions = "None"; // No warning/error since the compiler can prove a non-null assignment Console.WriteLine (myLicense.Restrictions.Length); } public class DriversLicense { public string Number { get; set; } public Image Photo { get; set;} public string? Restrictions { get; set; } }
Null Forgiveness Operator
#nullable enable string? foo = SomeMethodReturningNonnullString(); Console.WriteLine(foo!.Length); string? SomeMethodReturningNonnullString() { return "Bar"; }
Nullable Reference Types
Nullable Reference Types
#nullable enable // Enable nullable reference types void Main() { string s1 = null; // Generates a compiler warning! string? s2 = null; // OK: s2 is nullable reference type } class Foo { string x; // Generates a warning }
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); } void Main() { }
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: // (Note that these examples will not work in older versions of LINQPad) static void Main() { 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: static void Main() { 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: static void Main() { 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 StringHelper { public static void Foo (this UserQuery.Test 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: static void Main() { "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: static void Main() { 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
void Main() { 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
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
static (string,int) GetPerson() => ("Bob", 23); static void Main() { (string, int) person = GetPerson(); // Could use 'var' here if we want Console.WriteLine (person.Item1); // Bob Console.WriteLine (person.Item2); // 23 }
Naming tuple elements
var tuple = (Name:"Bob", Age:23); Console.WriteLine (tuple.Name); // Bob Console.WriteLine (tuple.Age); // 23
Naming tuple elements - types
static (string Name, int Age) GetPerson() => ("Bob", 23); static void Main() { var person = GetPerson(); Console.WriteLine (person.Name); // Bob Console.WriteLine (person.Age); // 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
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
static (string, int, char) GetBob() => ( "Bob", 23, 'M'); static void Main() { var (name, age, sex) = GetBob(); Console.WriteLine (name); // Bob Console.WriteLine (age); // 23 Console.WriteLine (sex); // 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!");
Attributes (see also CH19)
Attaching Attributes
void Main() { 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() { }
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 {} void Main() { }
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
void Main() { 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 { return customerName; } set { if (value == customerName) return; customerName = value; RaisePropertyChanged(); // The compiler converts the above line to: // RaisePropertyChanged ("CustomerName"); } } }
Dynamic Binding (see also CH20)
Custom Binding
// Custom binding occurs when a dynamic object implements IDynamicMetaObjectProvider: static void Main() { 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: static dynamic Mean (dynamic x, dynamic y) => (x + y) / 2; static void Main() { int x = 3, y = 4; Console.WriteLine (Mean (x, y)); }
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 interface IFoo { void Test(); } class Foo : IFoo { void IFoo.Test() {} } static void Main() { IFoo f = new Foo(); dynamic d = f; d.Test(); // Exception thrown }
Operator Overloading (see also CH6)
Operator Functions
// An operator is overloaded by declaring an operator function: public struct Note { int value; public int SemitonesFromA => value; public Note (int semitonesFromA) { value = semitonesFromA; } public static Note operator + (Note x, int semitones) { return new Note (x.value + semitones); } // Or more tersely: // 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 } static void Main() { Note B = new Note (2); Note CSharp = B + 2; CSharp.SemitonesFromA.Dump(); CSharp += 2; CSharp.SemitonesFromA.Dump(); }
Custom Implicit and Explicit Conversions
// Implicit and explicit conversions are overloadable operators: 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) ) )); } static void Main() { Note n = (Note)554.37; // explicit conversion double x = n; // implicit conversion x.Dump(); }
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: 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 void Main() { SqlBoolean a = SqlBoolean.Null; if (a) Console.WriteLine ("True"); else if (!a) Console.WriteLine ("False"); else Console.WriteLine ("Null"); }
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: 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; } } static void Main() { int[,] bitmap = { {0x101010, 0x808080, 0xFFFFFF}, {0x101010, 0x808080, 0xFFFFFF} }; BlueFilter (bitmap); bitmap.Dump(); }
Pinning variables with fixed
// Value types declared inline within reference types require the reference type to be pinned: class Test { public int X; } static void Main() { Test test = new Test(); unsafe { fixed (int* p = &test.X) // Pins test { *p = 9; } Console.WriteLine (test.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: struct Test { public int X; } unsafe static void Main() { Test test = new Test(); Test* p = &test; p->X = 9; Console.WriteLine (test.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: 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]; } } static void Main() { new UnsafeClass ("Christian Troy"); }
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: unsafe static void Main() { short[] a = {1,1,2,3,5,8,13,21,34,55}; 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 static void Zap (void* memory, int byteCount) { byte* b = (byte*) memory; for (int i = 0; i < byteCount; i++) *b++ = 0; }
Patterns
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
void Main() { 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
void Main() { 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: string { Length: 4 }, Port: 80 } => true, _ => false };
Property pattern with switch - with when clause
void Main() { 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 } when uri.Host.Length < 1000 => true, _ => false };
Property pattern with type pattern
void Main() { 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
void Main() { 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
void Main() { 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 };
Tuple patterns
void Main() { AverageCelsiusTemperature (Season.Spring, true).Dump(); } enum Season { Spring, Summer, Fall, Winter }; 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") };
Positional patterns
void Main() { Print (new Point (0, 0)).Dump(); } class Point { public readonly int X, Y; public Point (int x, int y) => (X, Y) = (x, y); public void Deconstruct (out int x, out int y) { x = X; y = Y; } } string Print (object obj) => obj switch { Point (0, 0) => "Empty point", Point (var x, var y) when x == y => "Diagonal", _ => "Other" };
var pattern
void Main() { Test (3, 3).Dump(); Test (4, 4).Dump(); Test (10, 10).Dump(); } bool Test (int x, int y) => x * y is var product && product > 10 && product < 100;
Constant pattern
void Main() { 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"); }
Switch Expressions
Positional Pattern
void Main() { var jax = new GameCharacter() { Role = GameRole.Figher, Level = 3 }; Console.WriteLine(GetTitle(jax)); } enum GameRole { Figher, Rogue, Mage }; class GameCharacter { public GameRole Role; public int Level; public void Deconstruct(out GameRole role, out int level) => (role, level) = (Role, Level); } string GetTitle(GameCharacter ch) => ch switch { var (role, level) when role == GameRole.Figher && level > 9 => "Knight", var (role, level) when role == GameRole.Figher && level > 2 => "Warrior", var (role, level) when role == GameRole.Figher => "Squire", var (role, level) when role == GameRole.Rogue && level > 9 => "Master", var (role, level) when role == GameRole.Rogue && level > 2 => "Footpad", var (role, level) when role == GameRole.Rogue => "Recuit", var (role, level) when role == GameRole.Mage && level > 9 => "Archmage", var (role, level) when role == GameRole.Mage && level > 2 => "Caster", var (role, level) when role == GameRole.Mage => "Apprentice", _ => throw new Exception("Unexpected values.") };
Property Patterns
void Main() { var jax = new GameCharacter() { Role = GameRole.Figher, Level = 3, Rested = true }; RollDamageIncludingWellRested (jax).Dump(); RollDamageIncludingWellRested (jax).Dump(); RollDamageIncludingWellRested (jax).Dump(); } Random rnd = new Random(); int RollDamage(GameCharacter ch) => ch switch { { Role: GameRole.Figher } => ch.Level * rnd.Next (10), { Role: GameRole.Rogue } => ch.Level * rnd.Next (6), { Role: GameRole.Mage } => ch.Level * rnd.Next (4), _ => throw new Exception("Unexpected condition.") }; int RollDamageIncludingWellRested (GameCharacter ch) => ch switch { { Role: GameRole.Figher, Rested: true } => ch.Level * rnd.Next (12), { Role: GameRole.Figher, Rested: false } => ch.Level * rnd.Next (10), { Role: GameRole.Rogue, Rested: true } => ch.Level * rnd.Next (7), { Role: GameRole.Rogue, Rested: false } => ch.Level * rnd.Next (6), // Well-rested doesn't affect mages { Role: GameRole.Mage } => ch.Level * rnd.Next (4), _ => throw new Exception ("Unexpected condition.") }; enum GameRole { Figher, Rogue, Mage }; class GameCharacter { public GameRole Role; public int Level; public bool Rested; }
Switch Expressions
int cardNumber = 12; string suite = "spades"; string cardRank = cardNumber switch { 13 => "King", 12 => "Queen", 11 => "Jack", _ => "Pip card" // equivalent to 'default' }; cardRank.Dump(); string cardName = (cardNumber, suite) switch { (13, "spades") => "King of spades", (13, "clubs") => "King of clubs", (13, "hearts") => "King of hearts", (13, "diamonds") => "King of diamonds", (12, "spades") => "Queen of spades", (12, "clubs") => "Queen of clubs", (12, "hearts") => "Queen of hearts", (12, "diamonds") => "Queen of diamonds", _ => "Something else" // The discard is also used for a tuple }; cardName.Dump();
Tuple Patterns
void Main() { var temp = AverageCelsiusTemperature(Season.Spring, daytime: true); Console.WriteLine(temp); // 20 } enum Season { Spring, Summer, Fall, Winter }; 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") };